@psnext/slingcli 2.5.20260722-2 → 2.5.20260722-3
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/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js +9 -7
- package/node_modules/@earendil-works/pi-agent-core/dist/agent.js +25 -23
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.js +32 -19
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/branch-summarization.js +6 -7
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js +75 -26
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/utils.js +6 -17
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl-storage.js +56 -4
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/memory-storage.js +56 -4
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/repo-utils.js +2 -2
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.js +30 -14
- package/node_modules/@earendil-works/pi-agent-core/dist/index.js +4 -2
- package/node_modules/@earendil-works/pi-agent-core/dist/stream-fn.js +17 -0
- package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js +2 -7
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +14 -2
- package/node_modules/@earendil-works/pi-ai/dist/api/pi-messages.js +2 -4
- package/node_modules/@earendil-works/pi-ai/dist/auth/helpers.js +3 -2
- package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/kimi-coding.js +249 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/load.js +5 -0
- package/node_modules/@earendil-works/pi-ai/dist/bun-oauth.js +2 -0
- package/node_modules/@earendil-works/pi-ai/dist/env-api-keys.js +2 -0
- package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js +60 -0
- package/node_modules/@earendil-works/pi-ai/dist/index.js +2 -0
- package/node_modules/@earendil-works/pi-ai/dist/models.generated.js +4 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/all.js +8 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.js +3 -2
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/cerebras.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-ai-gateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-workers-ai.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/fireworks.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/github-copilot.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/groq.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/huggingface.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/kimi-coding.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/moonshotai-cn.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/moonshotai.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai-codex.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/qwen-token-plan-cn.json +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/qwen-token-plan.json +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/together.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/xai.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.js +10 -2
- package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.js +2 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/qwen-token-plan-cn.js +15 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/qwen-token-plan-cn.models.js +5 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/qwen-token-plan.js +15 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/qwen-token-plan.models.js +5 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/radius-config.js +18 -14
- package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js +3 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js +79 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/text.js +10 -0
- package/node_modules/@earendil-works/{pi-agent-core/dist/harness/session → pi-ai/dist/utils}/uuid.js +4 -8
- package/node_modules/@earendil-works/pi-ai/package.json +8 -5
- package/node_modules/@earendil-works/pi-coding-agent/dist/cli/args.js +2 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js +72 -57
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/branch-summarization.js +8 -11
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/compaction.js +63 -26
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/utils.js +6 -17
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/runner.js +5 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-resolver.js +2 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-runtime.js +18 -15
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/project-trust.js +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/remote-catalog-provider.js +26 -7
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/sdk.js +6 -2
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/session-manager.js +100 -24
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/usage-totals.js +52 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/extensions/llama/client.js +3 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/extensions/llama/index.js +3 -3
- package/node_modules/@earendil-works/pi-coding-agent/dist/index.js +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/main.js +5 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js +20 -21
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +69 -25
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-client.js +7 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-mode.js +4 -0
- package/node_modules/@earendil-works/pi-coding-agent/npm-shrinkwrap.json +16 -31
- package/node_modules/@earendil-works/pi-coding-agent/package.json +4 -4
- package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/cli.js +5 -5
- package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/config.js +9 -9
- package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/ipc/client.js +1 -1
- package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/ipc/server.js +1 -1
- package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/radius.js +8 -8
- package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/rpc-process.js +1 -1
- package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/serve.js +3 -3
- package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/storage.js +7 -7
- package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/supervisor.js +2 -2
- package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/package.json +9 -6
- package/node_modules/@earendil-works/pi-tui/dist/components/editor.js +16 -10
- package/node_modules/@earendil-works/pi-tui/dist/tui.js +4 -2
- package/node_modules/@earendil-works/pi-tui/package.json +1 -1
- package/package.json +7 -7
- package/slingshot/index.js +253 -250
- /package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/handler.js +0 -0
- /package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/index.js +0 -0
- /package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/ipc/protocol.js +0 -0
- /package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/types.js +0 -0
|
@@ -32,7 +32,7 @@ export class ModelRuntime {
|
|
|
32
32
|
extensionProviders = new Map();
|
|
33
33
|
compositionErrors = new Map();
|
|
34
34
|
modelsPath;
|
|
35
|
-
|
|
35
|
+
modelNetworkEnabled;
|
|
36
36
|
config;
|
|
37
37
|
snapshot = {
|
|
38
38
|
all: [],
|
|
@@ -43,11 +43,11 @@ export class ModelRuntime {
|
|
|
43
43
|
};
|
|
44
44
|
availabilityRefresh;
|
|
45
45
|
availabilityError;
|
|
46
|
-
constructor(credentials, config, modelsPath, modelsStore, providers,
|
|
46
|
+
constructor(credentials, config, modelsPath, modelsStore, providers, modelNetworkEnabled) {
|
|
47
47
|
this.credentials = credentials;
|
|
48
48
|
this.config = config;
|
|
49
49
|
this.modelsPath = modelsPath;
|
|
50
|
-
this.
|
|
50
|
+
this.modelNetworkEnabled = modelNetworkEnabled;
|
|
51
51
|
this.defaultBuiltins = new Map(providers.map((provider) => [provider.id, provider]));
|
|
52
52
|
for (const [providerId, provider] of this.defaultBuiltins)
|
|
53
53
|
this.builtins.set(providerId, provider);
|
|
@@ -64,16 +64,19 @@ export class ModelRuntime {
|
|
|
64
64
|
: new InMemoryCodingAgentModelsStore());
|
|
65
65
|
const providers = builtinProviderCatalog
|
|
66
66
|
.builtinProviders()
|
|
67
|
-
.map((provider) => provider.id === "radius"
|
|
68
|
-
|
|
67
|
+
.map((provider) => provider.id === "radius"
|
|
68
|
+
? provider
|
|
69
|
+
: withRemoteCatalog(provider, options.catalogBaseUrl, builtinProviderCatalog.getBuiltinModelDataUrl(provider.id)));
|
|
70
|
+
const runtime = new ModelRuntime(credentials, config, modelsPath, modelsStore, providers, process.env.PI_OFFLINE === undefined);
|
|
69
71
|
runtime.configureRadiusProviders();
|
|
70
72
|
runtime.rebuildProviders();
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
+
const refreshFromNetwork = runtime.modelNetworkEnabled && options.allowModelNetwork === true;
|
|
74
|
+
const controller = refreshFromNetwork ? new AbortController() : undefined;
|
|
75
|
+
const timeout = controller
|
|
73
76
|
? setTimeout(() => controller.abort(), options.modelRefreshTimeoutMs ?? 15_000)
|
|
74
77
|
: undefined;
|
|
75
78
|
try {
|
|
76
|
-
await runtime.refresh({ allowNetwork:
|
|
79
|
+
await runtime.refresh({ allowNetwork: refreshFromNetwork, signal: controller?.signal });
|
|
77
80
|
}
|
|
78
81
|
finally {
|
|
79
82
|
if (timeout)
|
|
@@ -270,7 +273,7 @@ export class ModelRuntime {
|
|
|
270
273
|
},
|
|
271
274
|
};
|
|
272
275
|
}
|
|
273
|
-
async setRuntimeApiKey(providerId, apiKey) {
|
|
276
|
+
async setRuntimeApiKey(providerId, apiKey, refreshOptions = {}) {
|
|
274
277
|
this.credentials.setRuntimeApiKey(providerId, apiKey);
|
|
275
278
|
const auth = new Map(this.snapshot.auth).set(providerId, { type: "api_key", source: "runtime API key" });
|
|
276
279
|
const configuredProviders = new Set(this.snapshot.configuredProviders).add(providerId);
|
|
@@ -282,11 +285,11 @@ export class ModelRuntime {
|
|
|
282
285
|
storedProviders,
|
|
283
286
|
available: this.snapshot.all.filter((model) => configuredProviders.has(model.provider)),
|
|
284
287
|
};
|
|
285
|
-
await this.refresh(
|
|
288
|
+
await this.refresh(refreshOptions);
|
|
286
289
|
}
|
|
287
290
|
async removeRuntimeApiKey(providerId) {
|
|
288
291
|
this.credentials.removeRuntimeApiKey(providerId);
|
|
289
|
-
await this.refresh({ allowNetwork: this.
|
|
292
|
+
await this.refresh({ allowNetwork: this.modelNetworkEnabled });
|
|
290
293
|
}
|
|
291
294
|
listCredentials() {
|
|
292
295
|
return this.credentials.list();
|
|
@@ -347,25 +350,25 @@ export class ModelRuntime {
|
|
|
347
350
|
}
|
|
348
351
|
async login(providerId, type, interaction) {
|
|
349
352
|
const credential = await this.models.login(providerId, type, interaction);
|
|
350
|
-
await this.refresh({ allowNetwork: this.
|
|
353
|
+
await this.refresh({ allowNetwork: this.modelNetworkEnabled });
|
|
351
354
|
return credential;
|
|
352
355
|
}
|
|
353
356
|
async logout(providerId) {
|
|
354
357
|
await this.models.logout(providerId);
|
|
355
358
|
// Reset credential-dependent compatibility projections before the unconfigured provider is skipped by refresh.
|
|
356
359
|
this.recomposeProvider(providerId);
|
|
357
|
-
await this.refresh({ allowNetwork: this.
|
|
360
|
+
await this.refresh({ allowNetwork: this.modelNetworkEnabled });
|
|
358
361
|
}
|
|
359
362
|
async reloadConfig() {
|
|
360
363
|
this.config = await ModelConfig.load(this.modelsPath);
|
|
361
364
|
this.configureRadiusProviders();
|
|
362
365
|
this.rebuildProviders();
|
|
363
|
-
await this.refresh({ allowNetwork: this.
|
|
366
|
+
await this.refresh({ allowNetwork: this.modelNetworkEnabled });
|
|
364
367
|
}
|
|
365
368
|
async refresh(options = {}) {
|
|
366
369
|
const refreshOptions = {
|
|
367
370
|
...options,
|
|
368
|
-
allowNetwork: options.allowNetwork ?? this.
|
|
371
|
+
allowNetwork: options.allowNetwork ?? this.modelNetworkEnabled,
|
|
369
372
|
};
|
|
370
373
|
// Published pi-ai builds before ModelsStore returned void and accepted a provider ID.
|
|
371
374
|
// The fallback keeps source-mode CLI tests working without rebuilding workspace dependencies.
|
|
@@ -2,7 +2,7 @@ import { CONFIG_DIR_NAME } from "../config.js";
|
|
|
2
2
|
import { emitProjectTrustEvent } from "./extensions/runner.js";
|
|
3
3
|
import { getProjectTrustOptions, hasTrustRequiringProjectResources, } from "./trust-manager.js";
|
|
4
4
|
function formatProjectTrustPrompt(cwd) {
|
|
5
|
-
return `Trust project folder?\n${cwd}\n\nThis allows
|
|
5
|
+
return `Trust project folder?\n${cwd}\n\nThis allows pi to load ${CONFIG_DIR_NAME} settings and resources, install missing project packages, and execute project extensions.`;
|
|
6
6
|
}
|
|
7
7
|
async function selectProjectTrustOption(cwd, ctx) {
|
|
8
8
|
const options = getProjectTrustOptions(cwd, { includeSessionOnly: true });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
1
2
|
import { VERSION } from "../config.js";
|
|
2
3
|
import { getPiUserAgent } from "../utils/pi-user-agent.js";
|
|
3
4
|
const DEFAULT_CATALOG_BASE_URL = "https://pi.dev";
|
|
@@ -27,8 +28,17 @@ function parseCatalog(providerId, value) {
|
|
|
27
28
|
.filter((entry) => typeof entry === "object" && entry !== null && "id" in entry)
|
|
28
29
|
.map((model) => ({ ...model, provider: providerId }));
|
|
29
30
|
}
|
|
31
|
+
function remoteModels(entry, localLastModified) {
|
|
32
|
+
if (!entry)
|
|
33
|
+
return [];
|
|
34
|
+
if (localLastModified !== undefined &&
|
|
35
|
+
(entry.lastModified === undefined || entry.lastModified <= localLastModified)) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
return entry.models;
|
|
39
|
+
}
|
|
30
40
|
/** Add a persisted pi.dev catalog overlay to a static built-in provider. */
|
|
31
|
-
export function withRemoteCatalog(provider, catalogBaseUrl = DEFAULT_CATALOG_BASE_URL) {
|
|
41
|
+
export function withRemoteCatalog(provider, catalogBaseUrl = DEFAULT_CATALOG_BASE_URL, localCatalogUrl) {
|
|
32
42
|
let dynamicModels = [];
|
|
33
43
|
let inflightRefresh;
|
|
34
44
|
return {
|
|
@@ -37,13 +47,16 @@ export function withRemoteCatalog(provider, catalogBaseUrl = DEFAULT_CATALOG_BAS
|
|
|
37
47
|
refreshModels: (context) => {
|
|
38
48
|
inflightRefresh ??= (async () => {
|
|
39
49
|
try {
|
|
50
|
+
const localLastModified = localCatalogUrl
|
|
51
|
+
? await stat(localCatalogUrl).then((value) => value.mtimeMs, () => undefined)
|
|
52
|
+
: undefined;
|
|
40
53
|
const stored = await context.store.read();
|
|
41
|
-
|
|
42
|
-
dynamicModels = stored.models.filter((model) => model.provider === provider.id);
|
|
54
|
+
dynamicModels = remoteModels(stored, localLastModified).filter((model) => model.provider === provider.id);
|
|
43
55
|
if (!context.allowNetwork || context.signal?.aborted)
|
|
44
56
|
return;
|
|
45
57
|
if (!context.force &&
|
|
46
58
|
stored?.checkedAt !== undefined &&
|
|
59
|
+
stored.lastModified !== undefined &&
|
|
47
60
|
Date.now() - stored.checkedAt < REMOTE_CATALOG_REFRESH_INTERVAL_MS) {
|
|
48
61
|
return;
|
|
49
62
|
}
|
|
@@ -59,18 +72,24 @@ export function withRemoteCatalog(provider, catalogBaseUrl = DEFAULT_CATALOG_BAS
|
|
|
59
72
|
return;
|
|
60
73
|
const checkedAt = Date.now();
|
|
61
74
|
if (response.status === 404 || response.status === 501) {
|
|
62
|
-
await context.store.write({ models:
|
|
75
|
+
await context.store.write({ ...(stored ?? { models: [] }), checkedAt, lastModified: 0 });
|
|
63
76
|
return;
|
|
64
77
|
}
|
|
65
78
|
if (!response.ok) {
|
|
66
|
-
await context.store.write({ models:
|
|
79
|
+
await context.store.write({ ...(stored ?? { models: [] }), checkedAt });
|
|
67
80
|
throw new Error(`Model catalog request failed for ${provider.id}: ${response.status}`);
|
|
68
81
|
}
|
|
69
82
|
const refreshed = parseCatalog(provider.id, await response.json());
|
|
83
|
+
const lastModified = Date.parse(response.headers.get("last-modified") ?? "");
|
|
70
84
|
if (context.signal?.aborted)
|
|
71
85
|
return;
|
|
72
|
-
|
|
73
|
-
|
|
86
|
+
const entry = {
|
|
87
|
+
models: refreshed,
|
|
88
|
+
checkedAt,
|
|
89
|
+
lastModified: Number.isNaN(lastModified) ? 0 : lastModified,
|
|
90
|
+
};
|
|
91
|
+
dynamicModels = remoteModels(entry, localLastModified);
|
|
92
|
+
await context.store.write(entry);
|
|
74
93
|
}
|
|
75
94
|
finally {
|
|
76
95
|
inflightRefresh = undefined;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { Agent } from "@earendil-works/pi-agent-core";
|
|
3
|
-
import { clampThinkingLevel } from "@earendil-works/pi-ai/compat";
|
|
2
|
+
import { Agent, setDefaultStreamFn } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { clampThinkingLevel, streamSimple } from "@earendil-works/pi-ai/compat";
|
|
4
4
|
import { getAgentDir } from "../config.js";
|
|
5
5
|
import { resolvePath } from "../utils/paths.js";
|
|
6
6
|
import { AgentSession } from "./agent-session.js";
|
|
@@ -15,6 +15,10 @@ import { getDefaultSessionDir, SessionManager } from "./session-manager.js";
|
|
|
15
15
|
import { SettingsManager } from "./settings-manager.js";
|
|
16
16
|
import { time } from "./timings.js";
|
|
17
17
|
import { createBashTool, createCodingTools, createEditTool, createFindTool, createGrepTool, createLsTool, createReadOnlyTools, createReadTool, createWriteTool, withFileMutationQueue, } from "./tools/index.js";
|
|
18
|
+
// Preserve the pre-0.81 fallback for extensions that construct Agent instances
|
|
19
|
+
// or invoke low-level agent loops without supplying streamFn. Agent core remains
|
|
20
|
+
// provider-agnostic and does not import pi-ai/compat itself.
|
|
21
|
+
setDefaultStreamFn(streamSimple);
|
|
18
22
|
// Re-exports
|
|
19
23
|
export * from "./agent-session-runtime.js";
|
|
20
24
|
export { withFileMutationQueue,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { uuidv7 } from "@earendil-works/pi-
|
|
1
|
+
import { uuidv7 } from "@earendil-works/pi-ai";
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
3
|
import { appendFileSync, closeSync, createReadStream, existsSync, mkdirSync, openSync, readdirSync, readSync, statSync, writeFileSync, } from "fs";
|
|
4
4
|
import { readdir, stat } from "fs/promises";
|
|
@@ -253,6 +253,15 @@ export function getDefaultSessionDir(cwd, agentDir = getDefaultAgentDir()) {
|
|
|
253
253
|
return sessionDir;
|
|
254
254
|
}
|
|
255
255
|
const SESSION_READ_BUFFER_SIZE = 1024 * 1024;
|
|
256
|
+
const SESSION_HEADER_READ_BUFFER_SIZE = 4096;
|
|
257
|
+
/** Bound synchronous header discovery while allowing large cwd and custom metadata fields. */
|
|
258
|
+
const MAX_SESSION_HEADER_SCAN_BYTES = 1024 * 1024;
|
|
259
|
+
class SessionHeaderScanLimitError extends Error {
|
|
260
|
+
constructor(filePath) {
|
|
261
|
+
super(`Session header exceeds ${MAX_SESSION_HEADER_SCAN_BYTES}-byte scan limit: ${filePath}`);
|
|
262
|
+
this.name = "SessionHeaderScanLimitError";
|
|
263
|
+
}
|
|
264
|
+
}
|
|
256
265
|
function parseSessionEntryLine(line) {
|
|
257
266
|
if (!line.trim())
|
|
258
267
|
return null;
|
|
@@ -308,22 +317,70 @@ export function loadEntriesFromFile(filePath) {
|
|
|
308
317
|
}
|
|
309
318
|
return entries;
|
|
310
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Inspect a physical line while searching for the first parsed session entry.
|
|
322
|
+
* Blank and malformed lines are skipped to match loadEntriesFromFile().
|
|
323
|
+
* Returns undefined to keep scanning, null for a parsed non-header entry, or the header.
|
|
324
|
+
*/
|
|
325
|
+
function parseSessionHeaderCandidate(line) {
|
|
326
|
+
if (!line.trim())
|
|
327
|
+
return undefined;
|
|
328
|
+
const entry = parseSessionEntryLine(line);
|
|
329
|
+
if (!entry)
|
|
330
|
+
return undefined;
|
|
331
|
+
if (entry.type !== "session" || typeof entry.id !== "string")
|
|
332
|
+
return null;
|
|
333
|
+
return entry;
|
|
334
|
+
}
|
|
311
335
|
function readSessionHeader(filePath) {
|
|
336
|
+
const fd = openSync(filePath, "r");
|
|
312
337
|
try {
|
|
313
|
-
const
|
|
314
|
-
const buffer = Buffer.
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
338
|
+
const decoder = new StringDecoder("utf8");
|
|
339
|
+
const buffer = Buffer.allocUnsafe(SESSION_HEADER_READ_BUFFER_SIZE);
|
|
340
|
+
const lineChunks = [];
|
|
341
|
+
let scannedBytes = 0;
|
|
342
|
+
while (scannedBytes < MAX_SESSION_HEADER_SCAN_BYTES) {
|
|
343
|
+
const readLength = Math.min(buffer.length, MAX_SESSION_HEADER_SCAN_BYTES - scannedBytes);
|
|
344
|
+
const bytesRead = readSync(fd, buffer, 0, readLength, null);
|
|
345
|
+
if (bytesRead === 0) {
|
|
346
|
+
lineChunks.push(decoder.end());
|
|
347
|
+
return parseSessionHeaderCandidate(lineChunks.join("")) ?? null;
|
|
348
|
+
}
|
|
349
|
+
scannedBytes += bytesRead;
|
|
350
|
+
const chunk = decoder.write(buffer.subarray(0, bytesRead));
|
|
351
|
+
let lineStart = 0;
|
|
352
|
+
let newlineIndex = chunk.indexOf("\n", lineStart);
|
|
353
|
+
while (newlineIndex !== -1) {
|
|
354
|
+
lineChunks.push(chunk.slice(lineStart, newlineIndex));
|
|
355
|
+
const header = parseSessionHeaderCandidate(lineChunks.join(""));
|
|
356
|
+
if (header !== undefined)
|
|
357
|
+
return header;
|
|
358
|
+
lineChunks.length = 0;
|
|
359
|
+
lineStart = newlineIndex + 1;
|
|
360
|
+
newlineIndex = chunk.indexOf("\n", lineStart);
|
|
361
|
+
}
|
|
362
|
+
lineChunks.push(chunk.slice(lineStart));
|
|
323
363
|
}
|
|
324
|
-
|
|
364
|
+
// Probe for EOF so a final header without a newline is allowed when it ends
|
|
365
|
+
// exactly at the scan limit. Any additional byte exceeds the bounded scan.
|
|
366
|
+
const probe = Buffer.allocUnsafe(1);
|
|
367
|
+
if (readSync(fd, probe, 0, probe.length, null) === 0) {
|
|
368
|
+
lineChunks.push(decoder.end());
|
|
369
|
+
return parseSessionHeaderCandidate(lineChunks.join("")) ?? null;
|
|
370
|
+
}
|
|
371
|
+
throw new SessionHeaderScanLimitError(filePath);
|
|
372
|
+
}
|
|
373
|
+
finally {
|
|
374
|
+
closeSync(fd);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function readSessionHeaderForDiscovery(filePath) {
|
|
378
|
+
try {
|
|
379
|
+
return readSessionHeader(filePath);
|
|
325
380
|
}
|
|
326
381
|
catch {
|
|
382
|
+
// Discovery is best-effort: unreadable or oversized files are not sessions,
|
|
383
|
+
// and one corrupt file must not prevent other sessions from being found.
|
|
327
384
|
return null;
|
|
328
385
|
}
|
|
329
386
|
}
|
|
@@ -342,7 +399,7 @@ export function findMostRecentSession(sessionDir, cwd) {
|
|
|
342
399
|
const files = readdirSync(resolvedSessionDir)
|
|
343
400
|
.filter((f) => f.endsWith(".jsonl"))
|
|
344
401
|
.map((f) => join(resolvedSessionDir, f))
|
|
345
|
-
.map((path) => ({ path, header:
|
|
402
|
+
.map((path) => ({ path, header: readSessionHeaderForDiscovery(path) }))
|
|
346
403
|
.filter((file) => file.header !== null &&
|
|
347
404
|
(!resolvedCwd || sessionCwdMatches(getSessionHeaderCwd(file.header), resolvedCwd)))
|
|
348
405
|
.map(({ path }) => ({ path, mtime: statSync(path).mtime }))
|
|
@@ -350,6 +407,7 @@ export function findMostRecentSession(sessionDir, cwd) {
|
|
|
350
407
|
return files[0]?.path || null;
|
|
351
408
|
}
|
|
352
409
|
catch {
|
|
410
|
+
// Directory access and stat races make recent-session discovery unavailable.
|
|
353
411
|
return null;
|
|
354
412
|
}
|
|
355
413
|
}
|
|
@@ -535,7 +593,7 @@ export class SessionManager {
|
|
|
535
593
|
labelsById = new Map();
|
|
536
594
|
labelTimestampsById = new Map();
|
|
537
595
|
leafId = null;
|
|
538
|
-
constructor(cwd, sessionDir, sessionFile, persist, newSessionOptions) {
|
|
596
|
+
constructor(cwd, sessionDir, sessionFile, persist, newSessionOptions, preloadedFileEntries) {
|
|
539
597
|
this.cwd = resolvePath(cwd);
|
|
540
598
|
this.sessionDir = normalizePath(sessionDir);
|
|
541
599
|
this.persist = persist;
|
|
@@ -543,7 +601,7 @@ export class SessionManager {
|
|
|
543
601
|
mkdirSync(this.sessionDir, { recursive: true });
|
|
544
602
|
}
|
|
545
603
|
if (sessionFile) {
|
|
546
|
-
this.
|
|
604
|
+
this._setSessionFile(sessionFile, preloadedFileEntries);
|
|
547
605
|
}
|
|
548
606
|
else {
|
|
549
607
|
this.newSession(newSessionOptions);
|
|
@@ -551,15 +609,18 @@ export class SessionManager {
|
|
|
551
609
|
}
|
|
552
610
|
/** Switch to a different session file (used for resume and branching) */
|
|
553
611
|
setSessionFile(sessionFile) {
|
|
612
|
+
this._setSessionFile(sessionFile);
|
|
613
|
+
}
|
|
614
|
+
_setSessionFile(sessionFile, preloadedFileEntries) {
|
|
554
615
|
this.sessionFile = resolvePath(sessionFile);
|
|
555
616
|
if (existsSync(this.sessionFile)) {
|
|
556
|
-
this.fileEntries = loadEntriesFromFile(this.sessionFile);
|
|
617
|
+
this.fileEntries = preloadedFileEntries ?? loadEntriesFromFile(this.sessionFile);
|
|
557
618
|
// If file was empty, initialize it with a valid session header. If it was
|
|
558
619
|
// non-empty but did not parse as a pi session, fail without modifying it.
|
|
559
620
|
if (this.fileEntries.length === 0) {
|
|
560
621
|
const explicitPath = this.sessionFile;
|
|
561
622
|
if (statSync(explicitPath).size > 0) {
|
|
562
|
-
throw new Error(`Session file is not a valid
|
|
623
|
+
throw new Error(`Session file is not a valid pi session: ${explicitPath}`);
|
|
563
624
|
}
|
|
564
625
|
this.newSession();
|
|
565
626
|
this.sessionFile = explicitPath;
|
|
@@ -739,7 +800,7 @@ export class SessionManager {
|
|
|
739
800
|
return entry.id;
|
|
740
801
|
}
|
|
741
802
|
/** Append a compaction summary as child of current leaf, then advance leaf. Returns entry id. */
|
|
742
|
-
appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromHook) {
|
|
803
|
+
appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromHook, usage) {
|
|
743
804
|
const entry = {
|
|
744
805
|
type: "compaction",
|
|
745
806
|
id: generateId(this.byId),
|
|
@@ -749,6 +810,7 @@ export class SessionManager {
|
|
|
749
810
|
firstKeptEntryId,
|
|
750
811
|
tokensBefore,
|
|
751
812
|
details,
|
|
813
|
+
usage,
|
|
752
814
|
fromHook,
|
|
753
815
|
};
|
|
754
816
|
this._appendEntry(entry);
|
|
@@ -988,7 +1050,7 @@ export class SessionManager {
|
|
|
988
1050
|
* Same as branch(), but also appends a branch_summary entry that captures
|
|
989
1051
|
* context from the abandoned conversation path.
|
|
990
1052
|
*/
|
|
991
|
-
branchWithSummary(branchFromId, summary, details, fromHook) {
|
|
1053
|
+
branchWithSummary(branchFromId, summary, details, fromHook, usage) {
|
|
992
1054
|
if (branchFromId !== null && !this.byId.has(branchFromId)) {
|
|
993
1055
|
throw new Error(`Entry ${branchFromId} not found`);
|
|
994
1056
|
}
|
|
@@ -1001,6 +1063,7 @@ export class SessionManager {
|
|
|
1001
1063
|
fromId: branchFromId ?? "root",
|
|
1002
1064
|
summary,
|
|
1003
1065
|
details,
|
|
1066
|
+
usage,
|
|
1004
1067
|
fromHook,
|
|
1005
1068
|
};
|
|
1006
1069
|
this._appendEntry(entry);
|
|
@@ -1122,13 +1185,26 @@ export class SessionManager {
|
|
|
1122
1185
|
*/
|
|
1123
1186
|
static open(path, sessionDir, cwdOverride) {
|
|
1124
1187
|
const resolvedPath = resolvePath(path);
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1188
|
+
let header = null;
|
|
1189
|
+
let preloadedFileEntries;
|
|
1190
|
+
if (cwdOverride === undefined && existsSync(resolvedPath)) {
|
|
1191
|
+
try {
|
|
1192
|
+
header = readSessionHeader(resolvedPath);
|
|
1193
|
+
}
|
|
1194
|
+
catch (error) {
|
|
1195
|
+
if (!(error instanceof SessionHeaderScanLimitError))
|
|
1196
|
+
throw error;
|
|
1197
|
+
// The bounded scan is only a discovery optimization. A full load remains
|
|
1198
|
+
// authoritative for legacy files with very large headers or prefixes.
|
|
1199
|
+
preloadedFileEntries = loadEntriesFromFile(resolvedPath);
|
|
1200
|
+
const firstEntry = preloadedFileEntries[0];
|
|
1201
|
+
header = firstEntry?.type === "session" ? firstEntry : null;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
const cwd = cwdOverride ?? (header ? getSessionHeaderCwd(header) : undefined) ?? process.cwd();
|
|
1129
1205
|
// If no sessionDir provided, derive from file's parent directory
|
|
1130
1206
|
const dir = sessionDir ? normalizePath(sessionDir) : resolve(resolvedPath, "..");
|
|
1131
|
-
return new SessionManager(cwd, dir, resolvedPath, true);
|
|
1207
|
+
return new SessionManager(cwd, dir, resolvedPath, true, undefined, preloadedFileEntries);
|
|
1132
1208
|
}
|
|
1133
1209
|
/**
|
|
1134
1210
|
* Continue the most recent session, or create new if none.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export function createUsageTotals() {
|
|
2
|
+
return {
|
|
3
|
+
input: 0,
|
|
4
|
+
output: 0,
|
|
5
|
+
cacheRead: 0,
|
|
6
|
+
cacheWrite: 0,
|
|
7
|
+
cost: 0,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function addUsageToTotals(totals, usage) {
|
|
11
|
+
totals.input += usage.input;
|
|
12
|
+
totals.output += usage.output;
|
|
13
|
+
totals.cacheRead += usage.cacheRead;
|
|
14
|
+
totals.cacheWrite += usage.cacheWrite;
|
|
15
|
+
totals.cost += usage.cost.total;
|
|
16
|
+
}
|
|
17
|
+
/** Group attributable assistant usage by model and all other usage into a separate bucket. */
|
|
18
|
+
export function getUsageCostBreakdown(entries) {
|
|
19
|
+
const totalsByKey = new Map();
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
let key;
|
|
22
|
+
let usage;
|
|
23
|
+
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
24
|
+
key = `${entry.message.provider}/${entry.message.responseModel ?? entry.message.model}`;
|
|
25
|
+
usage = entry.message.usage;
|
|
26
|
+
}
|
|
27
|
+
else if (entry.type === "message" && entry.message.role === "toolResult" && entry.message.usage) {
|
|
28
|
+
key = "Tools/summaries";
|
|
29
|
+
usage = entry.message.usage;
|
|
30
|
+
}
|
|
31
|
+
else if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
|
|
32
|
+
key = "Tools/summaries";
|
|
33
|
+
usage = entry.usage;
|
|
34
|
+
}
|
|
35
|
+
if (!key || !usage)
|
|
36
|
+
continue;
|
|
37
|
+
let totals = totalsByKey.get(key);
|
|
38
|
+
if (!totals) {
|
|
39
|
+
totals = createUsageTotals();
|
|
40
|
+
totalsByKey.set(key, totals);
|
|
41
|
+
}
|
|
42
|
+
addUsageToTotals(totals, usage);
|
|
43
|
+
}
|
|
44
|
+
return Array.from(totalsByKey, ([key, totals]) => ({
|
|
45
|
+
key,
|
|
46
|
+
cost: totals.cost,
|
|
47
|
+
tokens: totals.input + totals.output + totals.cacheRead + totals.cacheWrite,
|
|
48
|
+
}))
|
|
49
|
+
.filter((entry) => entry.cost > 0 || entry.tokens > 0)
|
|
50
|
+
.sort((a, b) => b.cost - a.cost);
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=usage-totals.js.map
|
|
@@ -67,9 +67,11 @@ function parseLoadProgress(data) {
|
|
|
67
67
|
function parseDownloadProgress(data) {
|
|
68
68
|
if (typeof data !== "object" || data === null)
|
|
69
69
|
return undefined;
|
|
70
|
+
const nested = data.progress;
|
|
71
|
+
const files = typeof nested === "object" && nested !== null ? nested : data;
|
|
70
72
|
let done = 0;
|
|
71
73
|
let total = 0;
|
|
72
|
-
for (const value of Object.values(
|
|
74
|
+
for (const value of Object.values(files)) {
|
|
73
75
|
if (typeof value !== "object" || value === null)
|
|
74
76
|
continue;
|
|
75
77
|
const entry = value;
|
|
@@ -70,7 +70,7 @@ export default function llamaExtension(pi) {
|
|
|
70
70
|
model: target.id,
|
|
71
71
|
initialMessage: "Starting…",
|
|
72
72
|
cancelTitle: "Stop loading?",
|
|
73
|
-
cancelMessage:
|
|
73
|
+
cancelMessage: target.id,
|
|
74
74
|
run: (signal, update) => client.loadAndWait(target.id, update, signal),
|
|
75
75
|
cancel: () => client.unload(target.id),
|
|
76
76
|
});
|
|
@@ -96,7 +96,7 @@ export default function llamaExtension(pi) {
|
|
|
96
96
|
}
|
|
97
97
|
};
|
|
98
98
|
const unloadModel = async (ctx, ui, client, model) => {
|
|
99
|
-
if (!(await ui.confirm("Unload model?",
|
|
99
|
+
if (!(await ui.confirm("Unload model?", model.id)))
|
|
100
100
|
return;
|
|
101
101
|
await client.unloadAndWait(model.id);
|
|
102
102
|
await syncCatalog(ctx, client);
|
|
@@ -140,7 +140,7 @@ export default function llamaExtension(pi) {
|
|
|
140
140
|
model,
|
|
141
141
|
initialMessage: "Starting…",
|
|
142
142
|
cancelTitle: "Stop download?",
|
|
143
|
-
cancelMessage:
|
|
143
|
+
cancelMessage: model,
|
|
144
144
|
run: (signal, update) => client.downloadAndWait(model, update, signal),
|
|
145
145
|
cancel: () => client.unload(model),
|
|
146
146
|
});
|
|
@@ -5,7 +5,7 @@ export { CONFIG_DIR_NAME, getAgentDir, getDocsPath, getExamplesPath, getPackageD
|
|
|
5
5
|
export { AgentSession, parseSkillBlock, } from "./core/agent-session.js";
|
|
6
6
|
export { readStoredCredential } from "./core/auth-storage.js";
|
|
7
7
|
// Compaction
|
|
8
|
-
export { calculateContextTokens, collectEntriesForBranchSummary, compact, DEFAULT_COMPACTION_SETTINGS, estimateTokens, findCutPoint, findTurnStartIndex, generateBranchSummary, generateSummary, getLastAssistantUsage, prepareBranchEntries, serializeConversation, shouldCompact, } from "./core/compaction/index.js";
|
|
8
|
+
export { calculateContextTokens, collectEntriesForBranchSummary, compact, DEFAULT_COMPACTION_SETTINGS, estimateTokens, findCutPoint, findTurnStartIndex, generateBranchSummary, generateSummary, generateSummaryWithUsage, getLastAssistantUsage, prepareBranchEntries, serializeConversation, shouldCompact, } from "./core/compaction/index.js";
|
|
9
9
|
export { createEventBus } from "./core/event-bus.js";
|
|
10
10
|
export { createExtensionRuntime, defineTool, discoverAndLoadExtensions, ExtensionRunner, isBashToolResult, isEditToolResult, isFindToolResult, isGrepToolResult, isLsToolResult, isReadToolResult, isToolCallEventType, isWriteToolResult, wrapRegisteredTool, wrapRegisteredTools, } from "./core/extensions/index.js";
|
|
11
11
|
export { convertToLlm } from "./core/messages.js";
|
|
@@ -563,7 +563,7 @@ export async function main(args, options) {
|
|
|
563
563
|
});
|
|
564
564
|
}
|
|
565
565
|
else {
|
|
566
|
-
await modelRuntime.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey);
|
|
566
|
+
await modelRuntime.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey, { allowNetwork: false });
|
|
567
567
|
await services.modelRuntime.getAvailable();
|
|
568
568
|
}
|
|
569
569
|
}
|
|
@@ -647,6 +647,10 @@ export async function main(args, options) {
|
|
|
647
647
|
console.error(chalk.red("Error: PI_STARTUP_BENCHMARK only supports interactive mode"));
|
|
648
648
|
process.exit(1);
|
|
649
649
|
}
|
|
650
|
+
// RPC refreshes catalogs here in the background; interactive mode starts its refresh after TUI initialization.
|
|
651
|
+
if (!offlineMode && appMode === "rpc") {
|
|
652
|
+
void modelRuntime.refresh().catch(() => { });
|
|
653
|
+
}
|
|
650
654
|
if (appMode === "rpc") {
|
|
651
655
|
printTimings();
|
|
652
656
|
await runRpcMode(runtime);
|
package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
2
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
import { areExperimentalFeaturesEnabled } from "../../../core/experimental.js";
|
|
4
|
+
import { addUsageToTotals, createUsageTotals } from "../../../core/usage-totals.js";
|
|
4
5
|
import { theme } from "../theme/theme.js";
|
|
5
6
|
/**
|
|
6
7
|
* Sanitize text for display in a single-line status.
|
|
@@ -74,23 +75,21 @@ export class FooterComponent {
|
|
|
74
75
|
render(width) {
|
|
75
76
|
const state = this.session.state;
|
|
76
77
|
// Calculate cumulative usage from ALL session entries (not just post-compaction messages)
|
|
77
|
-
|
|
78
|
-
let totalOutput = 0;
|
|
79
|
-
let totalCacheRead = 0;
|
|
80
|
-
let totalCacheWrite = 0;
|
|
81
|
-
let totalCost = 0;
|
|
78
|
+
const usageTotals = createUsageTotals();
|
|
82
79
|
let latestCacheHitRate;
|
|
83
80
|
for (const entry of this.session.sessionManager.getEntries()) {
|
|
84
81
|
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
85
|
-
|
|
86
|
-
totalOutput += entry.message.usage.output;
|
|
87
|
-
totalCacheRead += entry.message.usage.cacheRead;
|
|
88
|
-
totalCacheWrite += entry.message.usage.cacheWrite;
|
|
89
|
-
totalCost += entry.message.usage.cost.total;
|
|
82
|
+
addUsageToTotals(usageTotals, entry.message.usage);
|
|
90
83
|
const latestPromptTokens = entry.message.usage.input + entry.message.usage.cacheRead + entry.message.usage.cacheWrite;
|
|
91
84
|
latestCacheHitRate =
|
|
92
85
|
latestPromptTokens > 0 ? (entry.message.usage.cacheRead / latestPromptTokens) * 100 : undefined;
|
|
93
86
|
}
|
|
87
|
+
else if (entry.type === "message" && entry.message.role === "toolResult" && entry.message.usage) {
|
|
88
|
+
addUsageToTotals(usageTotals, entry.message.usage);
|
|
89
|
+
}
|
|
90
|
+
else if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
|
|
91
|
+
addUsageToTotals(usageTotals, entry.usage);
|
|
92
|
+
}
|
|
94
93
|
}
|
|
95
94
|
// Calculate context usage from session (handles compaction correctly).
|
|
96
95
|
// After compaction, tokens are unknown until the next LLM response.
|
|
@@ -112,23 +111,23 @@ export class FooterComponent {
|
|
|
112
111
|
}
|
|
113
112
|
// Build stats line
|
|
114
113
|
const statsParts = [];
|
|
115
|
-
if (
|
|
116
|
-
statsParts.push(`↑${formatTokens(
|
|
117
|
-
if (
|
|
118
|
-
statsParts.push(`↓${formatTokens(
|
|
119
|
-
if (
|
|
120
|
-
statsParts.push(`R${formatTokens(
|
|
121
|
-
if (
|
|
122
|
-
statsParts.push(`W${formatTokens(
|
|
123
|
-
if ((
|
|
114
|
+
if (usageTotals.input)
|
|
115
|
+
statsParts.push(`↑${formatTokens(usageTotals.input)}`);
|
|
116
|
+
if (usageTotals.output)
|
|
117
|
+
statsParts.push(`↓${formatTokens(usageTotals.output)}`);
|
|
118
|
+
if (usageTotals.cacheRead)
|
|
119
|
+
statsParts.push(`R${formatTokens(usageTotals.cacheRead)}`);
|
|
120
|
+
if (usageTotals.cacheWrite)
|
|
121
|
+
statsParts.push(`W${formatTokens(usageTotals.cacheWrite)}`);
|
|
122
|
+
if ((usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) && latestCacheHitRate !== undefined) {
|
|
124
123
|
statsParts.push(`CH${latestCacheHitRate.toFixed(1)}%`);
|
|
125
124
|
}
|
|
126
125
|
// Kimi Coding is subscription-backed despite using API-key authentication.
|
|
127
126
|
const usingSubscription = state.model
|
|
128
127
|
? state.model.provider === "kimi-coding" || this.session.modelRuntime.isUsingOAuth(state.model.provider)
|
|
129
128
|
: false;
|
|
130
|
-
if (
|
|
131
|
-
const costStr = `$${
|
|
129
|
+
if (usageTotals.cost || usingSubscription) {
|
|
130
|
+
const costStr = `$${usageTotals.cost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`;
|
|
132
131
|
statsParts.push(costStr);
|
|
133
132
|
}
|
|
134
133
|
// Colorize context percentage based on usage
|