pi-spark 0.14.6 → 0.15.0
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 +1 -20
- package/assets/screenshot-credits.png +0 -0
- package/index.ts +0 -4
- package/package.json +3 -4
- package/src/config/index.ts +39 -64
- package/src/config/schema.ts +14 -17
- package/src/features/credits/index.ts +2 -1
- package/src/features/credits/manager.ts +9 -5
- package/src/features/credits/providers/moonshot.ts +1 -1
- package/src/features/credits/status.ts +1 -1
- package/src/features/credits/types.ts +1 -1
- package/src/features/editor/index.ts +1 -1
- package/src/features/footer/index.ts +8 -3
- package/src/features/fullscreen/filler.ts +1 -1
- package/src/features/presets/manager.ts +1 -1
- package/src/features/presets/selector.ts +6 -6
- package/src/utils/format.ts +1 -14
- package/src/utils/model.ts +6 -0
- package/src/utils/usage.ts +1 -1
- package/assets/screenshot-tools.png +0 -0
- package/src/features/pi/actions/models.ts +0 -165
- package/src/features/pi/actions/name.ts +0 -58
- package/src/features/pi/actions/whoami.ts +0 -61
- package/src/features/pi/config.ts +0 -3
- package/src/features/pi/index.ts +0 -23
- package/src/features/pi/model.ts +0 -56
- package/src/features/pi/registry.ts +0 -28
- package/src/features/web/actions/fetch.ts +0 -47
- package/src/features/web/actions/search.ts +0 -50
- package/src/features/web/client.ts +0 -77
- package/src/features/web/config.ts +0 -3
- package/src/features/web/index.ts +0 -28
- package/src/features/web/registry.ts +0 -22
- package/src/features/web/render.ts +0 -34
- package/src/utils/tool.ts +0 -221
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# pi-spark
|
|
4
4
|
|
|
5
|
-
[Pi](https://pi.dev/) package that polishes your daily experience
|
|
5
|
+
[Pi](https://pi.dev/) package that polishes your daily experience.
|
|
6
6
|
|
|
7
7
|

|
|
8
8
|
|
|
@@ -61,23 +61,6 @@ pi-spark generates a short recap of the current session after it goes idle, or o
|
|
|
61
61
|
|
|
62
62
|

|
|
63
63
|
|
|
64
|
-
### Agent tools: `pi` and `web`
|
|
65
|
-
|
|
66
|
-
pi-spark adds two agent tools:
|
|
67
|
-
|
|
68
|
-
The `pi` tool lets the pi coding agent inspect and manipulate itself (~740 tokens).
|
|
69
|
-
|
|
70
|
-
- `models` lists and searches the model catalog.
|
|
71
|
-
- `name` sets or updates the current session's name.
|
|
72
|
-
- `whoami` shows the current pi state, including session name, active model, and thinking level.
|
|
73
|
-
|
|
74
|
-
The `web` tool gives the agent live web access, backed by the free [Exa MCP](https://exa.ai/mcp) (~350 tokens). No separate MCP setup or API key is needed.
|
|
75
|
-
|
|
76
|
-
- `search` finds current information across the web and returns ready-to-use content.
|
|
77
|
-
- `fetch` reads the full content of known URLs as clean markdown, and can batch several URLs in one call.
|
|
78
|
-
|
|
79
|
-

|
|
80
|
-
|
|
81
64
|
## Configuration
|
|
82
65
|
|
|
83
66
|
pi-spark reads config from `~/.pi/agent/spark.json` and from the current project's `.pi/spark.json`. Project config overrides matching global fields.
|
|
@@ -121,10 +104,8 @@ All fields are optional. Each top-level feature runs with the defaults below unl
|
|
|
121
104
|
| `editor` | `EditorConfig` | Shows a working indicator and the current model on the editor's top border. |
|
|
122
105
|
| `footer` | `{}` | Shows session info, extension statuses, cost, and context usage on one line. |
|
|
123
106
|
| `fullscreen` | `{}` | Clears the screen and scrollback on start and exit, and pins the editor and footer to the bottom. |
|
|
124
|
-
| `pi` | `{}` | Exposes the `pi` agent tool (`models`, `name`, `whoami` actions). |
|
|
125
107
|
| `presets` | `{ [name]: Preset }` | Defines named model presets, keyed by name. |
|
|
126
108
|
| `recap` | `RecapConfig` | Generates a session recap when idle or on demand. |
|
|
127
|
-
| `web` | `{}` | Exposes the `web` agent tool (`search`, `fetch` actions) via Exa's hosted MCP server. |
|
|
128
109
|
|
|
129
110
|
#### `EditorConfig`
|
|
130
111
|
|
|
Binary file
|
package/index.ts
CHANGED
|
@@ -3,10 +3,8 @@ import { registerCredits } from "./src/features/credits";
|
|
|
3
3
|
import { registerEditor } from "./src/features/editor";
|
|
4
4
|
import { registerFooter } from "./src/features/footer";
|
|
5
5
|
import { registerFullscreen } from "./src/features/fullscreen";
|
|
6
|
-
import { registerPi } from "./src/features/pi";
|
|
7
6
|
import { registerPresets } from "./src/features/presets";
|
|
8
7
|
import { registerRecap } from "./src/features/recap";
|
|
9
|
-
import { registerWeb } from "./src/features/web";
|
|
10
8
|
|
|
11
9
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
10
|
|
|
@@ -24,8 +22,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
24
22
|
registerEditor(pi, events);
|
|
25
23
|
registerFooter(pi);
|
|
26
24
|
registerFullscreen(pi);
|
|
27
|
-
registerPi(pi);
|
|
28
25
|
registerPresets(pi);
|
|
29
26
|
registerRecap(pi);
|
|
30
|
-
registerWeb(pi);
|
|
31
27
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-spark",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Pi package that polishes your daily experience
|
|
3
|
+
"version": "0.15.0",
|
|
4
|
+
"description": "Pi package that polishes your daily experience.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-coding-agent",
|
|
7
7
|
"pi-package"
|
|
@@ -31,8 +31,7 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@grpc/grpc-js": "^1.14.4",
|
|
33
33
|
"@grpc/proto-loader": "^0.8.1",
|
|
34
|
-
"
|
|
35
|
-
"liqe": "^3.8.7",
|
|
34
|
+
"defu": "^6.1.7",
|
|
36
35
|
"ms": "4.0.0-nightly.202508271359",
|
|
37
36
|
"zod": "^4.4.3"
|
|
38
37
|
},
|
package/src/config/index.ts
CHANGED
|
@@ -1,27 +1,13 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { defu } from "defu";
|
|
4
5
|
|
|
5
|
-
import {
|
|
6
|
+
import { featureSchemas } from "./schema";
|
|
6
7
|
|
|
7
8
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
9
|
import type { SparkConfig } from "./schema";
|
|
9
10
|
|
|
10
|
-
type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
|
|
11
|
-
type JsonObject = { [key: string]: JsonValue };
|
|
12
|
-
|
|
13
|
-
/** All features disabled; used as the fallback when `spark.json` fails validation. */
|
|
14
|
-
const DISABLED_CONFIG: SparkConfig = Object.freeze({
|
|
15
|
-
credits: false,
|
|
16
|
-
editor: false,
|
|
17
|
-
footer: false,
|
|
18
|
-
fullscreen: false,
|
|
19
|
-
pi: false,
|
|
20
|
-
presets: false,
|
|
21
|
-
recap: false,
|
|
22
|
-
web: false,
|
|
23
|
-
});
|
|
24
|
-
|
|
25
11
|
const cache = new Map<string, SparkConfig>();
|
|
26
12
|
|
|
27
13
|
/** Load and validate spark.json once per session lifecycle; later calls return the cached result. */
|
|
@@ -30,66 +16,55 @@ export function loadConfig(ctx: ExtensionContext, fileName: string = "spark.json
|
|
|
30
16
|
const cached = cache.get(key);
|
|
31
17
|
if (cached) return cached;
|
|
32
18
|
|
|
33
|
-
|
|
34
|
-
|
|
19
|
+
// Deep-merge the global file under the project file, so project settings win at scalar
|
|
20
|
+
// leaves while deep objects (e.g., `recap.model`) combine across both.
|
|
21
|
+
const [globalPath, projectPath] = getConfigPaths(ctx.cwd, fileName);
|
|
22
|
+
const raw = defu(readRawConfig(projectPath) ?? {}, readRawConfig(globalPath) ?? {});
|
|
35
23
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
24
|
+
// Validate each feature independently so a single invalid field disables only that feature
|
|
25
|
+
// (falling back to its enabled defaults) instead of taking down the whole config.
|
|
26
|
+
const config = {} as Record<keyof SparkConfig, unknown>;
|
|
27
|
+
const errors: string[] = [];
|
|
39
28
|
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
for (const field of Object.keys(featureSchemas) as (keyof SparkConfig)[]) {
|
|
30
|
+
const value = raw[field];
|
|
42
31
|
|
|
43
|
-
|
|
44
|
-
|
|
32
|
+
if (value === undefined) {
|
|
33
|
+
config[field] = {};
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
45
36
|
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
if (value === false) {
|
|
38
|
+
config[field] = false;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
48
41
|
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
const result = featureSchemas[field].safeParse(value);
|
|
43
|
+
if (result.success) {
|
|
44
|
+
config[field] = result.data;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
51
47
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
48
|
+
config[field] = {};
|
|
49
|
+
errors.push(result.error.issues.map((issue) => `${[field, ...issue.path].join(".")}: ${issue.message}`).join("; "));
|
|
50
|
+
}
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const value = readJsonFile(path);
|
|
60
|
-
if (value === undefined) return;
|
|
52
|
+
if (errors.length > 0) {
|
|
53
|
+
ctx.ui.notify(`Invalid pi-spark config: ${errors.join("; ")}`, "error");
|
|
54
|
+
}
|
|
61
55
|
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
cache.set(key, config as SparkConfig);
|
|
57
|
+
return config as SparkConfig;
|
|
58
|
+
}
|
|
64
59
|
|
|
65
|
-
|
|
60
|
+
function getConfigPaths(cwd: string, fileName: string): [globalPath: string, projectPath: string] {
|
|
61
|
+
return [join(getAgentDir(), fileName), join(cwd, CONFIG_DIR_NAME, fileName)];
|
|
66
62
|
}
|
|
67
63
|
|
|
68
|
-
function
|
|
64
|
+
function readRawConfig(path: string): Record<string, unknown> | undefined {
|
|
69
65
|
try {
|
|
70
|
-
return JSON.parse(readFileSync(path, "utf8")) as
|
|
66
|
+
return JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
|
|
71
67
|
} catch {
|
|
72
68
|
return undefined;
|
|
73
69
|
}
|
|
74
70
|
}
|
|
75
|
-
|
|
76
|
-
function mergeConfig(base: JsonObject | undefined, override: JsonObject): JsonObject {
|
|
77
|
-
if (base === undefined) return override;
|
|
78
|
-
if (!isPlainObject(base) || !isPlainObject(override)) return override;
|
|
79
|
-
|
|
80
|
-
const result: Record<string, JsonValue> = { ...base };
|
|
81
|
-
Object.entries(override).forEach(([key, overrideValue]) => {
|
|
82
|
-
const baseValue = base[key];
|
|
83
|
-
if (isPlainObject(baseValue) && isPlainObject(overrideValue)) {
|
|
84
|
-
result[key] = { ...baseValue, ...overrideValue };
|
|
85
|
-
} else {
|
|
86
|
-
result[key] = overrideValue;
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
return result;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function isPlainObject(value: unknown): value is Record<string, JsonValue> {
|
|
94
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
95
|
-
}
|
package/src/config/schema.ts
CHANGED
|
@@ -4,27 +4,24 @@ import { creditsConfigSchema } from "../features/credits/config";
|
|
|
4
4
|
import { editorConfigSchema } from "../features/editor/config";
|
|
5
5
|
import { footerConfigSchema } from "../features/footer/config";
|
|
6
6
|
import { fullscreenConfigSchema } from "../features/fullscreen/config";
|
|
7
|
-
import { piConfigSchema } from "../features/pi/config";
|
|
8
7
|
import { presetsConfigSchema } from "../features/presets/config";
|
|
9
8
|
import { recapConfigSchema } from "../features/recap/config";
|
|
10
|
-
import { webConfigSchema } from "../features/web/config";
|
|
11
|
-
|
|
12
|
-
const disabled = z.literal(false);
|
|
13
9
|
|
|
14
10
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* Raw option shape for each feature. The enable/disable/default policy lives in `loadConfig`:
|
|
12
|
+
* an omitted field falls back to `{}` (enabled with defaults), `false` disables the feature, and
|
|
13
|
+
* any other value is validated against the feature schema.
|
|
17
14
|
*/
|
|
18
|
-
export const
|
|
19
|
-
credits: creditsConfigSchema
|
|
20
|
-
editor: editorConfigSchema
|
|
21
|
-
footer: footerConfigSchema
|
|
22
|
-
fullscreen: fullscreenConfigSchema
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
web: webConfigSchema.or(disabled).default({}),
|
|
27
|
-
});
|
|
15
|
+
export const featureSchemas = {
|
|
16
|
+
credits: creditsConfigSchema,
|
|
17
|
+
editor: editorConfigSchema,
|
|
18
|
+
footer: footerConfigSchema,
|
|
19
|
+
fullscreen: fullscreenConfigSchema,
|
|
20
|
+
presets: presetsConfigSchema,
|
|
21
|
+
recap: recapConfigSchema,
|
|
22
|
+
} as const;
|
|
28
23
|
|
|
29
24
|
/** Resolved config for every feature; `false` means the feature is disabled. */
|
|
30
|
-
export type SparkConfig =
|
|
25
|
+
export type SparkConfig = {
|
|
26
|
+
[K in keyof typeof featureSchemas]: z.infer<(typeof featureSchemas)[K]> | false;
|
|
27
|
+
};
|
|
@@ -2,8 +2,8 @@ import { CreditsManager } from "./manager";
|
|
|
2
2
|
import { loadConfig } from "../../config";
|
|
3
3
|
import { isUsage } from "../../utils/usage";
|
|
4
4
|
|
|
5
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
5
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
6
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
|
|
8
8
|
function hasCost(message: AgentMessage): boolean {
|
|
9
9
|
const usage = (message as { usage?: unknown }).usage;
|
|
@@ -44,6 +44,7 @@ export function registerCredits(pi: ExtensionAPI): void {
|
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
pi.on("session_shutdown", () => {
|
|
47
|
+
creditsManager?.cancel();
|
|
47
48
|
creditsManager = undefined;
|
|
48
49
|
});
|
|
49
50
|
}
|
|
@@ -36,25 +36,29 @@ export class CreditsManager {
|
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
cancel(): void {
|
|
40
|
+
this.inflight?.abort();
|
|
41
|
+
this.inflight = undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
39
44
|
private async fetch(ctx: ExtensionContext, provider: CreditsProvider, signal: AbortSignal): Promise<void> {
|
|
40
45
|
try {
|
|
41
46
|
const apiKey = await ctx.modelRegistry.getApiKeyForProvider(provider.id);
|
|
47
|
+
if (signal.aborted) return;
|
|
42
48
|
if (!apiKey) {
|
|
43
49
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
44
50
|
return;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
|
-
const signals = [AbortSignal.timeout(REQUEST_TIMEOUT_MS), signal];
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const credits = await provider.fetch(ctx, apiKey, AbortSignal.any(signals));
|
|
53
|
+
const signals = AbortSignal.any([AbortSignal.timeout(REQUEST_TIMEOUT_MS), signal]);
|
|
54
|
+
const credits = await provider.fetch(ctx, apiKey, signals);
|
|
51
55
|
|
|
52
56
|
// The active model may have changed while the request was in flight.
|
|
53
57
|
if (ctx.model?.provider !== provider.id) return;
|
|
54
58
|
|
|
55
59
|
ctx.ui.setStatus(STATUS_KEY, renderCredits(ctx.ui.theme, provider.label, credits));
|
|
56
60
|
} catch (error) {
|
|
57
|
-
if (signal.aborted
|
|
61
|
+
if (signal.aborted) return;
|
|
58
62
|
if (ctx.model?.provider !== provider.id) return;
|
|
59
63
|
|
|
60
64
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -10,7 +10,7 @@ interface MoonshotBalanceResponse {
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* For Moonshot, the international and China-mainland accounts live on separate hosts and bill in
|
|
13
|
-
* different currencies (USD vs CNY), which the endpoint does not report, so each
|
|
13
|
+
* different currencies (USD vs CNY), which the endpoint does not report, so each Pi provider ID
|
|
14
14
|
* fixes both host and currency.
|
|
15
15
|
*/
|
|
16
16
|
function createMoonshotProvider(id: string, host: string, currency: string): CreditsProvider {
|
|
@@ -14,7 +14,7 @@ export function renderCredits(theme: Theme, label: string, credits: Credits): st
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function renderError(theme: Theme, label: string, message: string): string {
|
|
17
|
-
return theme.fg("error", `${label}
|
|
17
|
+
return theme.fg("error", `${label} ${message}`);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function renderWindows(theme: Theme, credits: Extract<Credits, { type: "windows" }>): string {
|
|
@@ -16,7 +16,7 @@ export interface CreditsLane {
|
|
|
16
16
|
percent: number | undefined;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/** A credits source for a
|
|
19
|
+
/** A credits source for a Pi provider, shown in the status line while that provider is active. */
|
|
20
20
|
export interface CreditsProvider {
|
|
21
21
|
readonly id: Provider;
|
|
22
22
|
readonly label: string;
|
|
@@ -78,7 +78,7 @@ class Editor extends CustomEditor {
|
|
|
78
78
|
const modelBeforeText = this.slots.modelBefore;
|
|
79
79
|
const modelText = formatModel(this.ctx.model?.provider, this.ctx.model?.id, this.pi.getThinkingLevel());
|
|
80
80
|
|
|
81
|
-
return theme.fg("dim", [modelBeforeText, modelText].filter(Boolean).join("
|
|
81
|
+
return theme.fg("dim", [modelBeforeText, modelText].filter(Boolean).join(" · "));
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
@@ -5,6 +5,7 @@ import { pathToFileURL } from "node:url";
|
|
|
5
5
|
import { SplitLine } from "../../components/split-line";
|
|
6
6
|
import { loadConfig } from "../../config";
|
|
7
7
|
import { formatContextUsage, formatCost, formatCwd, linkText, sanitizeText } from "../../utils/format";
|
|
8
|
+
import { isFreeModel } from "../../utils/model";
|
|
8
9
|
import { getEntryUsage } from "../../utils/usage";
|
|
9
10
|
|
|
10
11
|
import type { ExtensionContext, ExtensionAPI, ReadonlyFooterDataProvider, Theme } from "@earendil-works/pi-coding-agent";
|
|
@@ -39,7 +40,7 @@ class FooterComponent implements Component {
|
|
|
39
40
|
const branch = this.footerData.getGitBranch();
|
|
40
41
|
const sessionName = this.ctx.sessionManager.getSessionName();
|
|
41
42
|
|
|
42
|
-
return this.theme.fg("dim", [cwdText, branch, sessionName].filter(Boolean).join("
|
|
43
|
+
return this.theme.fg("dim", [cwdText, branch, sessionName].filter(Boolean).join(" · "));
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
private getRight(): string {
|
|
@@ -47,7 +48,7 @@ class FooterComponent implements Component {
|
|
|
47
48
|
const styledCostText = this.getStyledCostText();
|
|
48
49
|
const styledContextUsageText = this.getStyledContextUsageText();
|
|
49
50
|
|
|
50
|
-
return [statusesText, styledCostText, styledContextUsageText].filter(Boolean).join(this.theme.fg("dim", "
|
|
51
|
+
return [statusesText, styledCostText, styledContextUsageText].filter(Boolean).join(this.theme.fg("dim", " · "));
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
/** Get extension statuses, sorted by key alphabetically. */
|
|
@@ -58,11 +59,15 @@ class FooterComponent implements Component {
|
|
|
58
59
|
return Array.from(extensionStatuses.entries())
|
|
59
60
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
60
61
|
.map(([, text]) => sanitizeText(text))
|
|
61
|
-
.join(this.theme.fg("dim", "
|
|
62
|
+
.join(this.theme.fg("dim", " · "));
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
private getStyledCostText(): string {
|
|
65
66
|
const totalCost = this.ctx.sessionManager.getBranch().reduce((acc, entry) => acc + (getEntryUsage(entry)?.cost.total ?? 0), 0);
|
|
67
|
+
|
|
68
|
+
// Hide cost when it's zero and the current model is free.
|
|
69
|
+
if (totalCost === 0 && this.ctx.model && isFreeModel(this.ctx.model)) return "";
|
|
70
|
+
|
|
66
71
|
const costText = formatCost(totalCost);
|
|
67
72
|
|
|
68
73
|
if (totalCost > 20) return this.theme.fg("warning", costText);
|
|
@@ -22,7 +22,7 @@ export class BottomFiller implements Component {
|
|
|
22
22
|
this.measuring = true;
|
|
23
23
|
|
|
24
24
|
// Re-assert `clearOnShrink` every render pass; the TUI reads it after `render()` returns,
|
|
25
|
-
// so this wins deterministically over
|
|
25
|
+
// so this wins deterministically over Pi's reset to the settings value on startup/reload.
|
|
26
26
|
this.tui.setClearOnShrink(true);
|
|
27
27
|
|
|
28
28
|
const rows = this.tui.terminal.rows;
|
|
@@ -95,7 +95,7 @@ export class PresetManager {
|
|
|
95
95
|
if (!preset) return;
|
|
96
96
|
|
|
97
97
|
return this.keys.find((key) => {
|
|
98
|
-
const p = this.presets[key]
|
|
98
|
+
const p = this.presets[key];
|
|
99
99
|
|
|
100
100
|
return p.provider === preset.provider && p.model === preset.model && p.thinkingLevel === preset.thinkingLevel;
|
|
101
101
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { keyHint, rawKeyHint, DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Box, Container, SelectList, Spacer, Text } from "@earendil-works/pi-tui";
|
|
3
3
|
|
|
4
4
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -39,11 +39,11 @@ export async function showPresetSelector(ctx: ExtensionContext, presetManager: P
|
|
|
39
39
|
box.addChild(new Spacer(1));
|
|
40
40
|
|
|
41
41
|
const keyHints = [
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
]
|
|
46
|
-
box.addChild(new Text(keyHints.
|
|
42
|
+
rawKeyHint("↑↓", "navigate"),
|
|
43
|
+
keyHint("tui.select.confirm", "select"),
|
|
44
|
+
keyHint("tui.select.cancel", "cancel"),
|
|
45
|
+
];
|
|
46
|
+
box.addChild(new Text(keyHints.join(" "), 0, 0));
|
|
47
47
|
|
|
48
48
|
container.addChild(box);
|
|
49
49
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
package/src/utils/format.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { Provider } from "@earendil-works/pi-ai";
|
|
4
4
|
import type { ContextUsage } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
|
|
6
6
|
export function formatModel(provider?: Provider, model?: string, thinkingLevel?: string): string {
|
|
@@ -28,11 +28,6 @@ export function formatCost(cost: number): string {
|
|
|
28
28
|
return `$${cost.toFixed(2)}`;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/** Format a duration in ms as `1.2s`, like the built-in bash tool. */
|
|
32
|
-
export function formatDuration(ms: number): string {
|
|
33
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
31
|
export function formatCwd(cwd: string, home: string): string {
|
|
37
32
|
const resolvedCwd = resolve(cwd);
|
|
38
33
|
const resolvedHome = resolve(home);
|
|
@@ -67,14 +62,6 @@ export function sanitizeText(text: string): string {
|
|
|
67
62
|
.trim();
|
|
68
63
|
}
|
|
69
64
|
|
|
70
|
-
/** Join the text blocks of tool result content into a single newline-separated string. */
|
|
71
|
-
export function joinTextContent(content: (TextContent | ImageContent)[]): string {
|
|
72
|
-
return content
|
|
73
|
-
.filter((block) => block.type === "text")
|
|
74
|
-
.map((block) => block.text)
|
|
75
|
-
.join("\n");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
65
|
/** Coerce a possibly-stringified numeric value to a finite number, or `undefined`. */
|
|
79
66
|
export function toNumber(value?: string | number | null): number | undefined {
|
|
80
67
|
if (value === undefined || value === null) return undefined;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
/** Check if a model has zero costs for all categories. */
|
|
4
|
+
export function isFreeModel(model: Model<Api>): boolean {
|
|
5
|
+
return model.cost.input === 0 && model.cost.output === 0 && model.cost.cacheRead === 0 && model.cost.cacheWrite === 0;
|
|
6
|
+
}
|
package/src/utils/usage.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Usage } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
|
|
4
|
-
/** Structural type guard for the
|
|
4
|
+
/** Structural type guard for the Pi `Usage` shape. */
|
|
5
5
|
export function isUsage(value: unknown): value is Usage {
|
|
6
6
|
if (typeof value !== "object" || value === null) return false;
|
|
7
7
|
|
|
Binary file
|