pi-spark 0.11.2 → 0.13.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 +76 -57
- package/assets/cover.png +0 -0
- package/index.ts +31 -0
- package/package.json +24 -9
- package/{extensions/shared → src}/components/inline-text.ts +1 -1
- package/{extensions/shared → src}/config/index.ts +37 -12
- package/src/config/schema.ts +30 -0
- package/{extensions/shared → src}/events/auto-collect-events.ts +1 -1
- package/src/events/index.ts +3 -0
- package/src/features/credits/config.ts +3 -0
- package/src/features/credits/index.ts +49 -0
- package/src/features/credits/manager.ts +64 -0
- package/src/features/credits/providers/deepseek.ts +36 -0
- package/src/features/credits/providers/fireworks.proto +42 -0
- package/src/features/credits/providers/fireworks.ts +111 -0
- package/src/features/credits/providers/index.ts +22 -0
- package/src/features/credits/providers/moonshot.ts +39 -0
- package/src/features/credits/providers/openai-codex.ts +62 -0
- package/src/features/credits/providers/openrouter.ts +35 -0
- package/src/features/credits/providers/vercel-ai-gateway.ts +30 -0
- package/src/features/credits/status.ts +43 -0
- package/src/features/credits/types.ts +24 -0
- package/{extensions → src/features}/editor/index.ts +8 -11
- package/{extensions → src/features}/footer/index.ts +7 -9
- package/{extensions → src/features}/fullscreen/index.ts +6 -9
- package/src/features/model/config.ts +3 -0
- package/{extensions/models → src/features/model}/index.ts +5 -5
- package/{extensions → src/features}/name/index.ts +6 -8
- package/{extensions → src/features}/presets/config.ts +1 -1
- package/{extensions → src/features}/presets/index.ts +4 -4
- package/{extensions → src/features}/presets/manager.ts +2 -2
- package/{extensions → src/features}/recap/config.ts +1 -1
- package/{extensions → src/features}/recap/index.ts +4 -6
- package/{extensions → src/features}/recap/manager.ts +5 -5
- package/{extensions → src/features}/recap/model.ts +2 -2
- package/{extensions/shared/format/index.ts → src/utils/format.ts} +29 -0
- package/{extensions/shared/usage/index.ts → src/utils/usage.ts} +0 -18
- package/extensions/models/config.ts +0 -3
- package/extensions/shared/config/schema.ts +0 -22
- package/extensions/shared/events/index.ts +0 -3
- /package/{extensions/shared → src}/components/split-line.ts +0 -0
- /package/{extensions/shared → src}/config/model.ts +0 -0
- /package/{extensions/shared → src}/events/preset.ts +0 -0
- /package/{extensions → src/features}/editor/config.ts +0 -0
- /package/{extensions → src/features}/editor/spinner.ts +0 -0
- /package/{extensions → src/features}/footer/config.ts +0 -0
- /package/{extensions → src/features}/fullscreen/config.ts +0 -0
- /package/{extensions → src/features}/fullscreen/filler.ts +0 -0
- /package/{extensions → src/features}/name/config.ts +0 -0
- /package/{extensions → src/features}/presets/selector.ts +0 -0
- /package/{extensions → src/features}/recap/idle.ts +0 -0
- /package/{extensions → src/features}/recap/widget.ts +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
|
|
3
|
+
import { credentials, loadPackageDefinition, Metadata } from "@grpc/grpc-js";
|
|
4
|
+
import { loadSync } from "@grpc/proto-loader";
|
|
5
|
+
|
|
6
|
+
import { toNumber } from "../../../utils/format";
|
|
7
|
+
|
|
8
|
+
import type { ClientUnaryCall, ServiceClientConstructor, ServiceError } from "@grpc/grpc-js";
|
|
9
|
+
import type { Credits, CreditsProvider } from "../types";
|
|
10
|
+
|
|
11
|
+
type GatewayClient = InstanceType<ServiceClientConstructor>;
|
|
12
|
+
|
|
13
|
+
const PROVIDER = "fireworks";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The control-plane gateway is a gRPC service, distinct from the inference API at
|
|
17
|
+
* `api.fireworks.ai`. Credit balance lives here, behind the `x-api-key` header.
|
|
18
|
+
*/
|
|
19
|
+
const TARGET = "gateway.fireworks.ai:443";
|
|
20
|
+
const DEADLINE_MS = 20_000;
|
|
21
|
+
|
|
22
|
+
interface Money {
|
|
23
|
+
currency_code?: string;
|
|
24
|
+
units?: string | number;
|
|
25
|
+
nanos?: string | number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface Balance {
|
|
29
|
+
money?: Money | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Account {
|
|
33
|
+
name?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ListAccountsResponse {
|
|
37
|
+
accounts?: Account[] | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** gRPC clients are reusable and multiplex over one connection, so build once. */
|
|
41
|
+
let client: GatewayClient | undefined;
|
|
42
|
+
|
|
43
|
+
/** The API key maps to a fixed account, so cache the resolved resource name. */
|
|
44
|
+
const accountByKey = new Map<string, string>();
|
|
45
|
+
|
|
46
|
+
function getClient(): GatewayClient {
|
|
47
|
+
if (client) return client;
|
|
48
|
+
|
|
49
|
+
const protoPath = fileURLToPath(new URL("./fireworks.proto", import.meta.url));
|
|
50
|
+
const definition = loadSync(protoPath, { keepCase: true, longs: String, defaults: true });
|
|
51
|
+
const proto = loadPackageDefinition(definition) as unknown as { gateway: { Gateway: ServiceClientConstructor } };
|
|
52
|
+
client = new proto.gateway.Gateway(TARGET, credentials.createSsl());
|
|
53
|
+
|
|
54
|
+
return client;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function unary<T>(method: string, request: object, apiKey: string, signal: AbortSignal): Promise<T> {
|
|
58
|
+
return new Promise<T>((resolve, reject) => {
|
|
59
|
+
const metadata = new Metadata();
|
|
60
|
+
metadata.set("x-api-key", apiKey);
|
|
61
|
+
|
|
62
|
+
const deadline = new Date(Date.now() + DEADLINE_MS);
|
|
63
|
+
const gateway = getClient();
|
|
64
|
+
const invoke = gateway[method] as (
|
|
65
|
+
request: object,
|
|
66
|
+
metadata: Metadata,
|
|
67
|
+
options: { deadline: Date },
|
|
68
|
+
callback: (error: ServiceError | null, response: T) => void,
|
|
69
|
+
) => ClientUnaryCall;
|
|
70
|
+
|
|
71
|
+
const call = invoke.call(gateway, request, metadata, { deadline }, (error, response) => {
|
|
72
|
+
if (error) reject(new Error(error.details || error.message));
|
|
73
|
+
else resolve(response);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (signal.aborted) call.cancel();
|
|
77
|
+
else signal.addEventListener("abort", () => call.cancel(), { once: true });
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function resolveAccount(apiKey: string, signal: AbortSignal): Promise<string> {
|
|
82
|
+
const cached = accountByKey.get(apiKey);
|
|
83
|
+
if (cached) return cached;
|
|
84
|
+
|
|
85
|
+
const response = await unary<ListAccountsResponse>("ListAccounts", {}, apiKey, signal);
|
|
86
|
+
const name = response.accounts?.[0]?.name;
|
|
87
|
+
if (!name) throw new Error("no account found");
|
|
88
|
+
|
|
89
|
+
accountByKey.set(apiKey, name);
|
|
90
|
+
return name;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function moneyToNumber(money: Money | null | undefined): number | undefined {
|
|
94
|
+
if (!money) return undefined;
|
|
95
|
+
|
|
96
|
+
const units = toNumber(money.units) ?? 0;
|
|
97
|
+
const nanos = toNumber(money.nanos) ?? 0;
|
|
98
|
+
return units + nanos / 1e9;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const fireworksProvider: CreditsProvider = {
|
|
102
|
+
id: PROVIDER,
|
|
103
|
+
label: "Fireworks",
|
|
104
|
+
|
|
105
|
+
async fetch(_ctx, apiKey, signal): Promise<Credits> {
|
|
106
|
+
const name = await resolveAccount(apiKey, signal);
|
|
107
|
+
const balance = await unary<Balance>("GetBalance", { name }, apiKey, signal);
|
|
108
|
+
|
|
109
|
+
return { type: "balance", remaining: moneyToNumber(balance.money) };
|
|
110
|
+
},
|
|
111
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { deepseekProvider } from "./deepseek";
|
|
2
|
+
import { fireworksProvider } from "./fireworks";
|
|
3
|
+
import { moonshotProvider, moonshotCnProvider } from "./moonshot";
|
|
4
|
+
import { openaiCodexProvider } from "./openai-codex";
|
|
5
|
+
import { openrouterProvider } from "./openrouter";
|
|
6
|
+
import { vercelAiGatewayProvider } from "./vercel-ai-gateway";
|
|
7
|
+
|
|
8
|
+
import type { CreditsProvider } from "../types";
|
|
9
|
+
|
|
10
|
+
const PROVIDERS: CreditsProvider[] = [
|
|
11
|
+
deepseekProvider,
|
|
12
|
+
fireworksProvider,
|
|
13
|
+
moonshotProvider,
|
|
14
|
+
moonshotCnProvider,
|
|
15
|
+
openaiCodexProvider,
|
|
16
|
+
openrouterProvider,
|
|
17
|
+
vercelAiGatewayProvider,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export function findProvider(provider?: string): CreditsProvider | undefined {
|
|
21
|
+
return PROVIDERS.find((entry) => entry.id === provider);
|
|
22
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { convertToUSD, toNumber } from "../../../utils/format";
|
|
2
|
+
|
|
3
|
+
import type { Credits, CreditsProvider } from "../types";
|
|
4
|
+
|
|
5
|
+
interface MoonshotBalanceResponse {
|
|
6
|
+
data?: {
|
|
7
|
+
available_balance?: string | number;
|
|
8
|
+
} | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
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 pi provider ID
|
|
14
|
+
* fixes both host and currency.
|
|
15
|
+
*/
|
|
16
|
+
function createMoonshotProvider(id: string, host: string, currency: string): CreditsProvider {
|
|
17
|
+
return {
|
|
18
|
+
id,
|
|
19
|
+
label: "Moonshot",
|
|
20
|
+
|
|
21
|
+
async fetch(_ctx, apiKey, signal): Promise<Credits> {
|
|
22
|
+
const headers: Record<string, string> = {
|
|
23
|
+
Accept: "application/json",
|
|
24
|
+
Authorization: `Bearer ${apiKey}`,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const response = await fetch(`https://${host}/v1/users/me/balance`, { headers, signal });
|
|
28
|
+
if (!response.ok) throw new Error("request failed");
|
|
29
|
+
|
|
30
|
+
const payload = (await response.json()) as MoonshotBalanceResponse;
|
|
31
|
+
const remaining = await convertToUSD(toNumber(payload.data?.available_balance), currency, signal);
|
|
32
|
+
|
|
33
|
+
return { type: "balance", remaining };
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const moonshotProvider = createMoonshotProvider("moonshotai", "api.moonshot.ai", "USD");
|
|
39
|
+
export const moonshotCnProvider = createMoonshotProvider("moonshotai-cn", "api.moonshot.cn", "CNY");
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { toNumber } from "../../../utils/format";
|
|
2
|
+
|
|
3
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { Credits, CreditsProvider } from "../types";
|
|
5
|
+
|
|
6
|
+
const PROVIDER = "openai-codex";
|
|
7
|
+
const URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
8
|
+
|
|
9
|
+
interface CodexUsageResponse {
|
|
10
|
+
rate_limit?: {
|
|
11
|
+
primary_window?: CodexRateWindow | null;
|
|
12
|
+
secondary_window?: CodexRateWindow | null;
|
|
13
|
+
} | null;
|
|
14
|
+
credits?: {
|
|
15
|
+
unlimited?: boolean;
|
|
16
|
+
} | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface CodexRateWindow {
|
|
20
|
+
used_percent?: number | string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getAccountId(ctx: ExtensionContext): string | undefined {
|
|
24
|
+
const credential = ctx.modelRegistry.authStorage.get(PROVIDER) as { accountId?: string } | undefined;
|
|
25
|
+
const accountId = credential?.accountId;
|
|
26
|
+
|
|
27
|
+
return typeof accountId === "string" && accountId.trim() ? accountId.trim() : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseUsedPercent(window?: CodexRateWindow | null): number | undefined {
|
|
31
|
+
const value = toNumber(window?.used_percent);
|
|
32
|
+
return typeof value === "number" ? Math.min(100, Math.max(0, value)) : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const openaiCodexProvider: CreditsProvider = {
|
|
36
|
+
id: PROVIDER,
|
|
37
|
+
label: "Codex",
|
|
38
|
+
|
|
39
|
+
async fetch(ctx, apiKey, signal): Promise<Credits> {
|
|
40
|
+
const headers: Record<string, string> = {
|
|
41
|
+
Accept: "application/json",
|
|
42
|
+
Authorization: `Bearer ${apiKey}`,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const accountId = getAccountId(ctx);
|
|
46
|
+
if (accountId) headers["ChatGPT-Account-Id"] = accountId;
|
|
47
|
+
|
|
48
|
+
const response = await fetch(URL, { headers, signal });
|
|
49
|
+
if (!response.ok) throw new Error("request failed");
|
|
50
|
+
|
|
51
|
+
const payload = (await response.json()) as CodexUsageResponse;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
type: "windows",
|
|
55
|
+
unlimited: payload.credits?.unlimited === true,
|
|
56
|
+
lanes: [
|
|
57
|
+
{ label: "5h", percent: parseUsedPercent(payload.rate_limit?.primary_window) },
|
|
58
|
+
{ label: "7d", percent: parseUsedPercent(payload.rate_limit?.secondary_window) },
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { toNumber } from "../../../utils/format";
|
|
2
|
+
|
|
3
|
+
import type { Credits, CreditsProvider } from "../types";
|
|
4
|
+
|
|
5
|
+
const PROVIDER = "openrouter";
|
|
6
|
+
const URL = "https://openrouter.ai/api/v1/credits";
|
|
7
|
+
|
|
8
|
+
interface OpenRouterCreditsResponse {
|
|
9
|
+
data?: {
|
|
10
|
+
total_credits?: number | null;
|
|
11
|
+
total_usage?: number | null;
|
|
12
|
+
} | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const openrouterProvider: CreditsProvider = {
|
|
16
|
+
id: PROVIDER,
|
|
17
|
+
label: "OpenRouter",
|
|
18
|
+
|
|
19
|
+
async fetch(_ctx, apiKey, signal): Promise<Credits> {
|
|
20
|
+
const headers: Record<string, string> = {
|
|
21
|
+
Accept: "application/json",
|
|
22
|
+
Authorization: `Bearer ${apiKey}`,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const response = await fetch(URL, { headers, signal });
|
|
26
|
+
if (!response.ok) throw new Error("request failed");
|
|
27
|
+
|
|
28
|
+
const payload = (await response.json()) as OpenRouterCreditsResponse;
|
|
29
|
+
const totalCredits = toNumber(payload.data?.total_credits);
|
|
30
|
+
const totalUsage = toNumber(payload.data?.total_usage);
|
|
31
|
+
const remaining = typeof totalCredits === "number" && typeof totalUsage === "number" ? totalCredits - totalUsage : undefined;
|
|
32
|
+
|
|
33
|
+
return { type: "balance", remaining };
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { toNumber } from "../../../utils/format";
|
|
2
|
+
|
|
3
|
+
import type { Credits, CreditsProvider } from "../types";
|
|
4
|
+
|
|
5
|
+
const PROVIDER = "vercel-ai-gateway";
|
|
6
|
+
const URL = "https://ai-gateway.vercel.sh/v1/credits";
|
|
7
|
+
|
|
8
|
+
interface VercelCreditsResponse {
|
|
9
|
+
balance?: string | number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const vercelAiGatewayProvider: CreditsProvider = {
|
|
13
|
+
id: PROVIDER,
|
|
14
|
+
label: "Vercel",
|
|
15
|
+
|
|
16
|
+
async fetch(_ctx, apiKey, signal): Promise<Credits> {
|
|
17
|
+
const headers: Record<string, string> = {
|
|
18
|
+
Accept: "application/json",
|
|
19
|
+
Authorization: `Bearer ${apiKey}`,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const response = await fetch(URL, { headers, signal });
|
|
23
|
+
if (!response.ok) throw new Error("request failed");
|
|
24
|
+
|
|
25
|
+
const payload = (await response.json()) as VercelCreditsResponse;
|
|
26
|
+
const remaining = toNumber(payload.balance);
|
|
27
|
+
|
|
28
|
+
return { type: "balance", remaining };
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Credits, CreditsLane } from "./types";
|
|
3
|
+
|
|
4
|
+
const WINDOWS_WARNING = 70;
|
|
5
|
+
const WINDOWS_ERROR = 90;
|
|
6
|
+
const BALANCE_WARNING = 10;
|
|
7
|
+
const BALANCE_ERROR = 5;
|
|
8
|
+
|
|
9
|
+
export function renderCredits(theme: Theme, label: string, credits: Credits): string {
|
|
10
|
+
const styledLabel = theme.fg("dim", label);
|
|
11
|
+
|
|
12
|
+
if (credits.type === "windows") return `${styledLabel} ${renderWindows(theme, credits)}`;
|
|
13
|
+
return `${styledLabel} ${renderBalance(theme, credits)}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function renderError(theme: Theme, label: string, message: string): string {
|
|
17
|
+
return theme.fg("error", `${label} credits unavailable: ${message}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function renderWindows(theme: Theme, credits: Extract<Credits, { type: "windows" }>): string {
|
|
21
|
+
const unlimited = credits.unlimited || credits.lanes.every((lane) => lane.percent === undefined);
|
|
22
|
+
if (unlimited) return theme.fg("success", "unlimited");
|
|
23
|
+
|
|
24
|
+
return credits.lanes.map((lane) => renderLane(theme, lane)).join(" ");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function renderLane(theme: Theme, lane: CreditsLane): string {
|
|
28
|
+
const text = `${lane.label} ${lane.percent === undefined ? "?" : lane.percent.toFixed(0)}%`;
|
|
29
|
+
|
|
30
|
+
if (lane.percent && lane.percent > WINDOWS_ERROR) return theme.fg("error", text);
|
|
31
|
+
if (lane.percent && lane.percent > WINDOWS_WARNING) return theme.fg("warning", text);
|
|
32
|
+
return theme.fg("success", text);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function renderBalance(theme: Theme, credits: Extract<Credits, { type: "balance" }>): string {
|
|
36
|
+
if (credits.remaining === undefined) return theme.fg("dim", "$?");
|
|
37
|
+
|
|
38
|
+
const text = `$${credits.remaining.toFixed(2)}`;
|
|
39
|
+
|
|
40
|
+
if (credits.remaining < BALANCE_ERROR) return theme.fg("error", text);
|
|
41
|
+
if (credits.remaining < BALANCE_WARNING) return theme.fg("warning", text);
|
|
42
|
+
return theme.fg("success", text);
|
|
43
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Provider } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Normalized credits/usage for a provider.
|
|
6
|
+
*
|
|
7
|
+
* - `balance` providers (e.g., OpenRouter, Vercel AI Gateway) report a remaining dollar balance.
|
|
8
|
+
* - `windows` providers (e.g., OpenAI Codex) report rate-limit windows as used percentages.
|
|
9
|
+
*/
|
|
10
|
+
export type Credits =
|
|
11
|
+
| { type: "balance"; remaining?: number | undefined }
|
|
12
|
+
| { type: "windows"; lanes: CreditsLane[]; unlimited?: boolean | undefined };
|
|
13
|
+
|
|
14
|
+
export interface CreditsLane {
|
|
15
|
+
label: string;
|
|
16
|
+
percent: number | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A credits source for a pi provider, shown in the status line while that provider is active. */
|
|
20
|
+
export interface CreditsProvider {
|
|
21
|
+
readonly id: Provider;
|
|
22
|
+
readonly label: string;
|
|
23
|
+
fetch(ctx: ExtensionContext, apiKey: string, signal: AbortSignal): Promise<Credits>;
|
|
24
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { CustomEditor } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
import { Spinner } from "./spinner";
|
|
4
|
-
import { SplitLine } from "
|
|
5
|
-
import { loadConfig } from "
|
|
6
|
-
import {
|
|
7
|
-
import { formatModel } from "
|
|
4
|
+
import { SplitLine } from "../../components/split-line";
|
|
5
|
+
import { loadConfig } from "../../config";
|
|
6
|
+
import { PRESET_CHANGE, parsePresetChange } from "../../events";
|
|
7
|
+
import { formatModel } from "../../utils/format";
|
|
8
8
|
|
|
9
9
|
import type { ExtensionAPI, ExtensionContext, KeybindingsManager } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import type { TUI, EditorTheme } from "@earendil-works/pi-tui";
|
|
11
|
+
import type { EventCollector } from "../../events";
|
|
11
12
|
|
|
12
13
|
class Editor extends CustomEditor {
|
|
13
14
|
private pi: ExtensionAPI;
|
|
@@ -81,18 +82,14 @@ class Editor extends CustomEditor {
|
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
export
|
|
85
|
-
const events = autoCollectEvents(pi);
|
|
86
|
-
|
|
85
|
+
export function registerEditor(pi: ExtensionAPI, events: EventCollector): void {
|
|
87
86
|
let editor: Editor | undefined = undefined;
|
|
88
87
|
let spinner: Spinner | undefined = undefined;
|
|
89
88
|
let runningToolCallIds = new Set<string>();
|
|
90
89
|
|
|
91
90
|
pi.on("session_start", (_event, ctx) => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const config = loadConfig(ctx, "editor");
|
|
95
|
-
if (!config) return;
|
|
91
|
+
const config = loadConfig(ctx).editor;
|
|
92
|
+
if (!ctx.hasUI || !config) return;
|
|
96
93
|
|
|
97
94
|
spinner = new Spinner(config.spinner);
|
|
98
95
|
|
|
@@ -2,10 +2,10 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
|
|
5
|
-
import { SplitLine } from "
|
|
6
|
-
import { loadConfig } from "
|
|
7
|
-
import { formatContextUsage, formatCost, formatCwd, linkText, sanitizeText } from "
|
|
8
|
-
import { getEntryUsage } from "
|
|
5
|
+
import { SplitLine } from "../../components/split-line";
|
|
6
|
+
import { loadConfig } from "../../config";
|
|
7
|
+
import { formatContextUsage, formatCost, formatCwd, linkText, sanitizeText } from "../../utils/format";
|
|
8
|
+
import { getEntryUsage } from "../../utils/usage";
|
|
9
9
|
|
|
10
10
|
import type { ExtensionContext, ExtensionAPI, ReadonlyFooterDataProvider, Theme } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import type { Component } from "@earendil-works/pi-tui";
|
|
@@ -80,12 +80,10 @@ class FooterComponent implements Component {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
export
|
|
83
|
+
export function registerFooter(pi: ExtensionAPI): void {
|
|
84
84
|
pi.on("session_start", (_event, ctx) => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const config = loadConfig(ctx, "footer");
|
|
88
|
-
if (!config) return;
|
|
85
|
+
const config = loadConfig(ctx).footer;
|
|
86
|
+
if (!ctx.hasUI || !config) return;
|
|
89
87
|
|
|
90
88
|
ctx.ui.setFooter((_tui, theme, footerData) => new FooterComponent(ctx, theme, footerData));
|
|
91
89
|
});
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { VERSION } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
3
|
|
|
3
4
|
import { BottomFiller } from "./filler";
|
|
4
|
-
import { loadConfig } from "
|
|
5
|
-
import { sanitizeText } from "
|
|
5
|
+
import { loadConfig } from "../../config";
|
|
6
|
+
import { sanitizeText } from "../../utils/format";
|
|
6
7
|
|
|
7
8
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
9
|
-
|
|
10
9
|
import type { TUI } from "@earendil-works/pi-tui";
|
|
11
10
|
import type { UserMessage } from "@earendil-works/pi-ai";
|
|
12
11
|
|
|
@@ -33,16 +32,14 @@ function extractText(message: UserMessage): string {
|
|
|
33
32
|
return content.filter((block) => block.type === "text").map((block) => block.text).join(" ");
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
export
|
|
35
|
+
export function registerFullscreen(pi: ExtensionAPI): void {
|
|
37
36
|
let tui: TUI | undefined;
|
|
38
37
|
let enabled = false;
|
|
39
38
|
let pendingClear = false;
|
|
40
39
|
|
|
41
40
|
pi.on("session_start", (_event, ctx) => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const config = loadConfig(ctx, "fullscreen");
|
|
45
|
-
if (!config) return;
|
|
41
|
+
const config = loadConfig(ctx).fullscreen;
|
|
42
|
+
if (!ctx.hasUI || !config) return;
|
|
46
43
|
|
|
47
44
|
enabled = true;
|
|
48
45
|
pendingClear = true;
|
|
@@ -4,8 +4,8 @@ import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
|
4
4
|
import { filter, parse } from "liqe";
|
|
5
5
|
import { Type } from "typebox";
|
|
6
6
|
|
|
7
|
-
import { loadConfig } from "
|
|
8
|
-
import { formatModel, formatTokens } from "
|
|
7
|
+
import { loadConfig } from "../../config";
|
|
8
|
+
import { formatModel, formatTokens } from "../../utils/format";
|
|
9
9
|
|
|
10
10
|
import type { Api, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
11
11
|
import type { ExtensionAPI, TruncationResult } from "@earendil-works/pi-coding-agent";
|
|
@@ -72,9 +72,9 @@ function formatListNotice(truncated: boolean, startIndex: number, endDisplay: nu
|
|
|
72
72
|
return undefined;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
export
|
|
75
|
+
export function registerModel(pi: ExtensionAPI): void {
|
|
76
76
|
pi.on("session_start", (_event, ctx) => {
|
|
77
|
-
const config = loadConfig(ctx
|
|
77
|
+
const config = loadConfig(ctx).model;
|
|
78
78
|
if (!config) return;
|
|
79
79
|
|
|
80
80
|
pi.registerTool({
|
|
@@ -89,7 +89,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
89
89
|
promptSnippet: "Show the active model or list pi models",
|
|
90
90
|
promptGuidelines: [
|
|
91
91
|
"Use the model tool when you need pi model metadata, the active model, or the thinking level, rather than guessing.",
|
|
92
|
-
"
|
|
92
|
+
"Use the model tool with `available:true` in the query unless unavailable models are explicitly needed.",
|
|
93
93
|
],
|
|
94
94
|
parameters: Type.Object({
|
|
95
95
|
action: StringEnum(["active", "list"] as const, {
|
|
@@ -1,25 +1,23 @@
|
|
|
1
1
|
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
|
|
4
|
-
import { loadConfig } from "
|
|
5
|
-
import { sanitizeText } from "
|
|
4
|
+
import { loadConfig } from "../../config";
|
|
5
|
+
import { sanitizeText } from "../../utils/format";
|
|
6
6
|
|
|
7
7
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
|
|
9
|
-
export
|
|
9
|
+
export function registerName(pi: ExtensionAPI): void {
|
|
10
10
|
pi.on("session_start", (_event, ctx) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const config = loadConfig(ctx, "name");
|
|
11
|
+
const config = loadConfig(ctx).name;
|
|
14
12
|
if (!config) return;
|
|
15
13
|
|
|
16
14
|
pi.registerTool({
|
|
17
15
|
name: "name",
|
|
18
16
|
label: "name",
|
|
19
17
|
description:
|
|
20
|
-
"
|
|
18
|
+
"Set or update the current session's name. The name is a concise label shown in the session " +
|
|
21
19
|
"selector instead of the first-message preview.",
|
|
22
|
-
promptSnippet: "
|
|
20
|
+
promptSnippet: "Set or update the current session's name",
|
|
23
21
|
promptGuidelines: [
|
|
24
22
|
"Use the name tool when the session needs a concise, recognizable label, especially after a long, vague, or pasted opening prompt.",
|
|
25
23
|
"Use the name tool to rename the session only after a substantial shift in the conversation's focus, not for minor follow-ups.",
|
|
@@ -2,11 +2,11 @@ import { Key } from "@earendil-works/pi-tui";
|
|
|
2
2
|
|
|
3
3
|
import { PresetManager } from "./manager";
|
|
4
4
|
import { showPresetSelector } from "./selector";
|
|
5
|
-
import { loadConfig } from "
|
|
5
|
+
import { loadConfig } from "../../config";
|
|
6
6
|
|
|
7
7
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
|
|
9
|
-
export
|
|
9
|
+
export function registerPresets(pi: ExtensionAPI): void {
|
|
10
10
|
let presetManager: PresetManager | undefined = undefined;
|
|
11
11
|
|
|
12
12
|
pi.registerFlag("preset", {
|
|
@@ -14,10 +14,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
14
14
|
type: "string",
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
pi.on("session_start", async (
|
|
17
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
18
|
+
const config = loadConfig(ctx).presets;
|
|
18
19
|
const presetFlag = pi.getFlag("preset");
|
|
19
20
|
|
|
20
|
-
const config = loadConfig(ctx, "presets");
|
|
21
21
|
if (!config || Object.keys(config).length === 0) {
|
|
22
22
|
if (presetFlag) ctx.ui.notify("No presets defined in spark.json", "warning");
|
|
23
23
|
return;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { PRESET_CHANGE } from "
|
|
2
|
-
import { formatModel } from "
|
|
1
|
+
import { PRESET_CHANGE } from "../../events";
|
|
2
|
+
import { formatModel } from "../../utils/format";
|
|
3
3
|
|
|
4
4
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import type { PresetsConfig, PresetConfig } from "./config";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
|
|
3
3
|
import { idleTimeoutSchema } from "./idle";
|
|
4
|
-
import { optionalModelSchema } from "
|
|
4
|
+
import { optionalModelSchema } from "../../config/model";
|
|
5
5
|
|
|
6
6
|
export const recapConfigSchema = optionalModelSchema.extend({
|
|
7
7
|
idle: idleTimeoutSchema.optional(),
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import { IdleListener } from "./idle";
|
|
2
2
|
import { RecapManager } from "./manager";
|
|
3
|
-
import { loadConfig } from "
|
|
3
|
+
import { loadConfig } from "../../config";
|
|
4
4
|
|
|
5
5
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
|
|
7
|
-
export
|
|
7
|
+
export function registerRecap(pi: ExtensionAPI): void {
|
|
8
8
|
let idleListener: IdleListener<ExtensionContext> | undefined = undefined;
|
|
9
9
|
let recapManager: RecapManager | undefined = undefined;
|
|
10
10
|
|
|
11
11
|
pi.on("session_start", (event, ctx) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const config = loadConfig(ctx, "recap");
|
|
15
|
-
if (!config) return;
|
|
12
|
+
const config = loadConfig(ctx).recap;
|
|
13
|
+
if (!ctx.hasUI || !config) return;
|
|
16
14
|
|
|
17
15
|
recapManager = new RecapManager(pi, config);
|
|
18
16
|
|
|
@@ -3,22 +3,22 @@ import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-a
|
|
|
3
3
|
|
|
4
4
|
import { resolveRecapModelSettings } from "./model";
|
|
5
5
|
import { clearRecapWidget, setRecapLoadingWidget, setRecapTextWidget } from "./widget";
|
|
6
|
-
import { sanitizeText } from "
|
|
6
|
+
import { sanitizeText } from "../../utils/format";
|
|
7
7
|
|
|
8
8
|
import type { Api, Model, ModelThinkingLevel, SimpleStreamOptions, Usage } from "@earendil-works/pi-ai";
|
|
9
9
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import type { RecapConfig } from "./config";
|
|
11
11
|
|
|
12
12
|
const SYSTEM_PROMPT = [
|
|
13
|
-
"You write concise idle
|
|
13
|
+
"You write concise idle session recaps for a terminal coding agent.",
|
|
14
14
|
"Use only transcript-supported facts; do not invent progress, intent, files, or next steps.",
|
|
15
15
|
"Prefer the latest active task if the session changed direction.",
|
|
16
16
|
"Summarize the user's goal, what was done, current state, and any clearly supported next step.",
|
|
17
|
-
"Respond in the conversation's primary language.",
|
|
18
|
-
"Output 1-2 plain-text sentences under
|
|
17
|
+
"Respond in the conversation's primary language. Address the user in the second person.",
|
|
18
|
+
"Output 1-2 plain-text sentences under 40 words. No heading, markdown, bullets, or quotes.",
|
|
19
19
|
].join("\n");
|
|
20
20
|
|
|
21
|
-
const MAX_TOKENS =
|
|
21
|
+
const MAX_TOKENS = 80;
|
|
22
22
|
const MAX_CONVERSATION_CHARS = 8_000;
|
|
23
23
|
|
|
24
24
|
export class RecapManager {
|