pi-spark 0.19.2 → 0.20.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 +30 -2
- package/package.json +1 -1
- package/src/features/credits/config.ts +18 -1
- package/src/features/credits/index.ts +6 -3
- package/src/features/credits/manager.ts +6 -1
- package/src/features/credits/providers/index.ts +10 -4
- package/src/features/credits/providers/kimi-code.ts +59 -16
- package/src/features/credits/providers/moonshot.ts +1 -1
- package/src/features/credits/types.ts +2 -2
- package/src/features/editor/config.ts +3 -1
- package/src/features/editor/spinner.ts +2 -7
- package/src/features/footer/config.ts +7 -1
- package/src/features/footer/index.ts +19 -9
- package/src/features/fullscreen/filler.ts +42 -0
- package/src/features/fullscreen/index.ts +12 -8
- package/src/features/presets/config.ts +2 -1
- package/src/features/presets/manager.ts +1 -1
- package/src/utils/model.ts +0 -5
package/README.md
CHANGED
|
@@ -120,14 +120,33 @@ All fields are optional. Each top-level feature runs with the defaults below unl
|
|
|
120
120
|
|
|
121
121
|
| Field | Value (or `false`) | Description |
|
|
122
122
|
| --- | --- | --- |
|
|
123
|
-
| `credits` | `
|
|
123
|
+
| `credits` | `CreditsConfig` | Shows the active provider's credit balance or rate-limit usage in the status line. |
|
|
124
124
|
| `editor` | `EditorConfig` | Shows a working indicator and the current model on the editor's top border. |
|
|
125
|
-
| `footer` | `
|
|
125
|
+
| `footer` | `FooterConfig` | Shows session info, extension statuses, cost, and context usage. |
|
|
126
126
|
| `fullscreen` | `{}` | Clears the screen and scrollback on start and exit, and pins the editor and footer to the bottom. |
|
|
127
127
|
| `presets` | `{ [name]: Preset }` | Defines named model presets, keyed by name. |
|
|
128
128
|
| `recap` | `RecapConfig` | Generates a session recap when idle or on demand. |
|
|
129
129
|
| `title` | `TitleConfig` | Names the session automatically after the first exchange. |
|
|
130
130
|
|
|
131
|
+
#### `CreditsConfig`
|
|
132
|
+
|
|
133
|
+
All supported providers are enabled by default. Set a provider to `false` to disable its credits status, or to `true` to override a global `false` in project config.
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"credits": {
|
|
138
|
+
"providers": {
|
|
139
|
+
"fireworks": false,
|
|
140
|
+
"openai-codex": true
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
| Field | Value | Description |
|
|
147
|
+
| --- | --- | --- |
|
|
148
|
+
| `providers` | partial map of provider IDs to booleans | Enables or disables credits for individual providers. Valid IDs: `deepseek`, `fireworks`, `kimi-coding`, `moonshotai`, `moonshotai-cn`, `openai-codex`, `openrouter`, and `vercel-ai-gateway`. |
|
|
149
|
+
|
|
131
150
|
#### `EditorConfig`
|
|
132
151
|
|
|
133
152
|
The `spinner` field is optional and defaults to `tildes`.
|
|
@@ -139,6 +158,15 @@ The `spinner` field is optional and defaults to `tildes`.
|
|
|
139
158
|
| | `tildes` (default) | `∼, ≈, ≋, ≈, ∼` |
|
|
140
159
|
| | `pulse` | `·, •, ●, •, ·` |
|
|
141
160
|
|
|
161
|
+
#### `FooterConfig`
|
|
162
|
+
|
|
163
|
+
The `statusPosition` field is optional and defaults to `inline`.
|
|
164
|
+
|
|
165
|
+
| Field | Value | Description |
|
|
166
|
+
| --- | --- | --- |
|
|
167
|
+
| `statusPosition` | `inline` (default) | Shows extension statuses on the same line as session info, cost, and context usage. |
|
|
168
|
+
| | `below` | Moves extension statuses to a new line below. |
|
|
169
|
+
|
|
142
170
|
#### `Preset`
|
|
143
171
|
|
|
144
172
|
Each preset must set all three fields.
|
package/package.json
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const creditsProviderIdSchema = z.enum([
|
|
4
|
+
"deepseek",
|
|
5
|
+
"fireworks",
|
|
6
|
+
"kimi-coding",
|
|
7
|
+
"moonshotai",
|
|
8
|
+
"moonshotai-cn",
|
|
9
|
+
"openai-codex",
|
|
10
|
+
"openrouter",
|
|
11
|
+
"vercel-ai-gateway",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export type CreditsProviderId = z.infer<typeof creditsProviderIdSchema>;
|
|
15
|
+
|
|
16
|
+
export const creditsConfigSchema = z.object({
|
|
17
|
+
providers: z.partialRecord(creditsProviderIdSchema, z.boolean()).optional(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export type CreditsConfig = z.infer<typeof creditsConfigSchema>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CreditsManager } from "./manager";
|
|
2
|
-
import { registerProviderExtensions } from "./providers";
|
|
2
|
+
import { getEnabledProviders, registerProviderExtensions } from "./providers";
|
|
3
3
|
import { loadConfig } from "../../config";
|
|
4
4
|
import { isUsage } from "../../utils/usage";
|
|
5
5
|
|
|
@@ -20,8 +20,11 @@ export function registerCredits(pi: ExtensionAPI): void {
|
|
|
20
20
|
const config = loadConfig(ctx).credits;
|
|
21
21
|
if (!ctx.hasUI || !config) return;
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
const providers = getEnabledProviders(config);
|
|
24
|
+
if (providers.length === 0) return;
|
|
25
|
+
|
|
26
|
+
creditsManager = new CreditsManager(providers);
|
|
27
|
+
registerProviderExtensions(pi, ctx, providers, async (ctx) => await creditsManager?.refresh(ctx));
|
|
25
28
|
|
|
26
29
|
creditsManager.refresh(ctx);
|
|
27
30
|
});
|
|
@@ -9,12 +9,17 @@ const REQUEST_TIMEOUT_MS = 30_000;
|
|
|
9
9
|
|
|
10
10
|
export class CreditsManager {
|
|
11
11
|
private inflight: AbortController | undefined = undefined;
|
|
12
|
+
private providers: CreditsProvider[];
|
|
12
13
|
private currentProvider: string | undefined = undefined;
|
|
13
14
|
|
|
15
|
+
constructor(providers: CreditsProvider[]) {
|
|
16
|
+
this.providers = providers;
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
async refresh(ctx: ExtensionContext): Promise<void> {
|
|
15
20
|
this.inflight?.abort();
|
|
16
21
|
|
|
17
|
-
const provider = findProvider(ctx.model?.provider);
|
|
22
|
+
const provider = findProvider(this.providers, ctx.model?.provider);
|
|
18
23
|
if (!provider) {
|
|
19
24
|
this.inflight = undefined;
|
|
20
25
|
this.currentProvider = undefined;
|
|
@@ -7,6 +7,7 @@ import { openrouterProvider } from "./openrouter";
|
|
|
7
7
|
import { vercelAiGatewayProvider } from "./vercel-ai-gateway";
|
|
8
8
|
|
|
9
9
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import type { CreditsConfig } from "../config";
|
|
10
11
|
import type { CreditsProvider, RefreshCredits } from "../types";
|
|
11
12
|
|
|
12
13
|
const PROVIDERS: CreditsProvider[] = [
|
|
@@ -20,10 +21,15 @@ const PROVIDERS: CreditsProvider[] = [
|
|
|
20
21
|
vercelAiGatewayProvider,
|
|
21
22
|
];
|
|
22
23
|
|
|
23
|
-
export function
|
|
24
|
-
|
|
24
|
+
export function getEnabledProviders(config: CreditsConfig): CreditsProvider[] {
|
|
25
|
+
const settings = config.providers as Partial<Record<string, boolean>> | undefined;
|
|
26
|
+
return PROVIDERS.filter((provider) => settings?.[provider.id] !== false);
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
export function
|
|
28
|
-
|
|
29
|
+
export function findProvider(providers: CreditsProvider[], provider?: string): CreditsProvider | undefined {
|
|
30
|
+
return providers.find((entry) => entry.id === provider);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function registerProviderExtensions(pi: ExtensionAPI, ctx: ExtensionContext, providers: CreditsProvider[], refresh: RefreshCredits): void {
|
|
34
|
+
for (const provider of providers) provider.register?.(pi, ctx, refresh);
|
|
29
35
|
}
|
|
@@ -4,18 +4,45 @@ import type { Credits, CreditsLane, CreditsProvider } from "../types";
|
|
|
4
4
|
|
|
5
5
|
const PROVIDER = "kimi-coding";
|
|
6
6
|
const URL = "https://api.kimi.com/coding/v1/usages";
|
|
7
|
+
const BOOSTER_FIXED_POINT_CENTS = 1_000_000;
|
|
8
|
+
|
|
9
|
+
interface KimiCodeUsage {
|
|
10
|
+
limit?: string | number;
|
|
11
|
+
used?: string | number;
|
|
12
|
+
remaining?: string | number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface KimiCodeLimit {
|
|
16
|
+
window?: { duration?: number; timeUnit?: string } | null;
|
|
17
|
+
detail?: KimiCodeUsage | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface KimiCodeBoosterWallet {
|
|
21
|
+
balance?: {
|
|
22
|
+
type?: string;
|
|
23
|
+
amount?: string | number;
|
|
24
|
+
amountLeft?: string | number;
|
|
25
|
+
} | null;
|
|
26
|
+
}
|
|
7
27
|
|
|
8
28
|
interface KimiCodeUsageResponse {
|
|
9
|
-
usage?:
|
|
10
|
-
limits?:
|
|
11
|
-
|
|
12
|
-
detail?: { used?: string | number } | null;
|
|
13
|
-
}[] | null;
|
|
29
|
+
usage?: KimiCodeUsage | null;
|
|
30
|
+
limits?: KimiCodeLimit[] | null;
|
|
31
|
+
boosterWallet?: KimiCodeBoosterWallet | null;
|
|
14
32
|
}
|
|
15
33
|
|
|
16
|
-
function toPercent(
|
|
17
|
-
|
|
18
|
-
|
|
34
|
+
function toPercent(detail: KimiCodeUsage): number | undefined {
|
|
35
|
+
const limit = toNumber(detail.limit);
|
|
36
|
+
let used = toNumber(detail.used);
|
|
37
|
+
|
|
38
|
+
if (used === undefined && limit !== undefined) {
|
|
39
|
+
const remaining = toNumber(detail.remaining);
|
|
40
|
+
if (remaining !== undefined) used = limit - remaining;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (used === undefined || limit === undefined || limit <= 0) return undefined;
|
|
44
|
+
|
|
45
|
+
return Math.min(100, Math.max(0, (used / limit) * 100));
|
|
19
46
|
}
|
|
20
47
|
|
|
21
48
|
function formatWindowLabel(window?: { duration?: number; timeUnit?: string } | null): string {
|
|
@@ -34,8 +61,20 @@ function formatWindowLabel(window?: { duration?: number; timeUnit?: string } | n
|
|
|
34
61
|
return `${duration}${unit.replace("TIME_UNIT_", "").toLowerCase()}`;
|
|
35
62
|
}
|
|
36
63
|
|
|
37
|
-
function buildLane(label: string, detail
|
|
38
|
-
|
|
64
|
+
function buildLane(label: string, detail: KimiCodeUsage): CreditsLane | undefined {
|
|
65
|
+
const percent = toPercent(detail);
|
|
66
|
+
return percent === undefined ? undefined : { label, percent };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatSuffix(wallet?: KimiCodeBoosterWallet | null): string | undefined {
|
|
70
|
+
if (wallet?.balance?.type !== "BOOSTER") return undefined;
|
|
71
|
+
|
|
72
|
+
const amount = toNumber(wallet.balance.amount);
|
|
73
|
+
if (amount === undefined || amount <= 0) return undefined;
|
|
74
|
+
|
|
75
|
+
const amountLeft = Math.max(0, toNumber(wallet.balance.amountLeft) ?? 0);
|
|
76
|
+
const remaining = Math.round(amountLeft / BOOSTER_FIXED_POINT_CENTS) / 100;
|
|
77
|
+
return `(Extra Usage $${remaining.toFixed(2)})`;
|
|
39
78
|
}
|
|
40
79
|
|
|
41
80
|
export const kimiCodeProvider: CreditsProvider = {
|
|
@@ -55,15 +94,19 @@ export const kimiCodeProvider: CreditsProvider = {
|
|
|
55
94
|
const lanes: CreditsLane[] = [];
|
|
56
95
|
|
|
57
96
|
payload.limits?.forEach((item) => {
|
|
58
|
-
if (!item) return;
|
|
97
|
+
if (!item.detail) return;
|
|
59
98
|
|
|
60
|
-
const
|
|
61
|
-
lanes.push(
|
|
99
|
+
const lane = buildLane(formatWindowLabel(item.window), item.detail);
|
|
100
|
+
if (lane) lanes.push(lane);
|
|
62
101
|
});
|
|
63
102
|
|
|
64
|
-
|
|
65
|
-
|
|
103
|
+
if (payload.usage) {
|
|
104
|
+
const weeklyLane = buildLane("7d", payload.usage);
|
|
105
|
+
if (weeklyLane) lanes.push(weeklyLane);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (lanes.length === 0) throw new Error("no usage data");
|
|
66
109
|
|
|
67
|
-
return { type: "windows", lanes };
|
|
110
|
+
return { type: "windows", lanes, suffix: formatSuffix(payload.boosterWallet) };
|
|
68
111
|
},
|
|
69
112
|
};
|
|
@@ -13,7 +13,7 @@ interface MoonshotBalanceResponse {
|
|
|
13
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
|
-
function createMoonshotProvider(id:
|
|
16
|
+
function createMoonshotProvider(id: "moonshotai" | "moonshotai-cn", host: string, currency: string): CreditsProvider {
|
|
17
17
|
return {
|
|
18
18
|
id,
|
|
19
19
|
label: "Moonshot",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ProviderId } from "@earendil-works/pi-ai";
|
|
2
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { CreditsProviderId } from "./config";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Normalized credits/usage for a provider.
|
|
@@ -21,7 +21,7 @@ export type RefreshCredits = (ctx: ExtensionContext) => Promise<void>;
|
|
|
21
21
|
|
|
22
22
|
/** A credits source for a Pi provider, shown in the status line while that provider is active. */
|
|
23
23
|
export interface CreditsProvider {
|
|
24
|
-
readonly id:
|
|
24
|
+
readonly id: CreditsProviderId;
|
|
25
25
|
readonly label: string;
|
|
26
26
|
fetch(apiKey: string, signal: AbortSignal): Promise<Credits>;
|
|
27
27
|
register?(pi: ExtensionAPI, ctx: ExtensionContext, refresh: RefreshCredits): void;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const spinnerPresetSchema = z.enum(["dots", "lights", "tildes", "pulse"]);
|
|
4
|
+
|
|
5
|
+
export type SpinnerPreset = z.infer<typeof spinnerPresetSchema>;
|
|
4
6
|
|
|
5
7
|
export const editorConfigSchema = z.object({
|
|
6
8
|
spinner: spinnerPresetSchema.optional(),
|
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import * as z from "zod";
|
|
2
|
-
|
|
3
1
|
import type { TUI } from "@earendil-works/pi-tui";
|
|
4
|
-
|
|
5
|
-
export const spinnerPresetSchema = z.enum(["dots", "lights", "tildes", "pulse"]);
|
|
6
|
-
|
|
7
|
-
type SpinnerPreset = z.infer<typeof spinnerPresetSchema>;
|
|
2
|
+
import type { SpinnerPreset } from "./config";
|
|
8
3
|
|
|
9
4
|
interface SpinnerParams {
|
|
10
5
|
frames: string[];
|
|
@@ -35,7 +30,7 @@ const SPINNER_PRESETS: Record<SpinnerPreset, SpinnerParams> = {
|
|
|
35
30
|
},
|
|
36
31
|
};
|
|
37
32
|
|
|
38
|
-
const DEFAULT_SPINNER_PRESET = "tildes";
|
|
33
|
+
const DEFAULT_SPINNER_PRESET: SpinnerPreset = "tildes";
|
|
39
34
|
|
|
40
35
|
export class Spinner {
|
|
41
36
|
private tui: TUI | undefined;
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const statusPositionSchema = z.enum(["inline", "below"]);
|
|
4
|
+
|
|
5
|
+
export type StatusPosition = z.infer<typeof statusPositionSchema>;
|
|
6
|
+
|
|
7
|
+
export const footerConfigSchema = z.object({
|
|
8
|
+
statusPosition: statusPositionSchema.optional(),
|
|
9
|
+
});
|
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { hyperlink, Text } from "@earendil-works/pi-tui";
|
|
4
5
|
|
|
5
6
|
import { SplitLine } from "../../components/split-line";
|
|
6
7
|
import { loadConfig } from "../../config";
|
|
7
8
|
import { formatContextUsage, formatCost, formatCwd, sanitizeText } from "../../utils/format";
|
|
8
|
-
import { isFreeModel } from "../../utils/model";
|
|
9
9
|
import { getEntryUsage } from "../../utils/usage";
|
|
10
10
|
|
|
11
|
-
import { hyperlink } from "@earendil-works/pi-tui";
|
|
12
|
-
|
|
13
11
|
import type { ExtensionContext, ExtensionAPI, ReadonlyFooterDataProvider, Theme } from "@earendil-works/pi-coding-agent";
|
|
14
12
|
import type { Component } from "@earendil-works/pi-tui";
|
|
13
|
+
import type { StatusPosition } from "./config";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_STATUS_POSITION: StatusPosition = "inline";
|
|
15
16
|
|
|
16
17
|
class FooterComponent implements Component {
|
|
17
18
|
private ctx: ExtensionContext;
|
|
18
19
|
private theme: Theme;
|
|
19
20
|
private footerData: ReadonlyFooterDataProvider;
|
|
21
|
+
private statusPosition: StatusPosition;
|
|
20
22
|
|
|
21
|
-
constructor(ctx: ExtensionContext, theme: Theme, footerData: ReadonlyFooterDataProvider) {
|
|
23
|
+
constructor(ctx: ExtensionContext, theme: Theme, footerData: ReadonlyFooterDataProvider, statusPosition: StatusPosition = DEFAULT_STATUS_POSITION) {
|
|
22
24
|
this.ctx = ctx;
|
|
23
25
|
this.theme = theme;
|
|
24
26
|
this.footerData = footerData;
|
|
27
|
+
this.statusPosition = statusPosition;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
invalidate(): void {
|
|
@@ -32,7 +35,14 @@ class FooterComponent implements Component {
|
|
|
32
35
|
const left = this.getLeft();
|
|
33
36
|
const right = this.getRight();
|
|
34
37
|
|
|
35
|
-
|
|
38
|
+
const lines = new SplitLine(left, right, { primarySide: "right", ellipsis: this.theme.fg("dim", "…") }).render(width);
|
|
39
|
+
|
|
40
|
+
if (this.statusPosition === "below") {
|
|
41
|
+
const statusesText = this.getStatusesText();
|
|
42
|
+
if (statusesText) lines.push(...new Text(statusesText, 0, 0).render(width));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return lines;
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
private getLeft(): string {
|
|
@@ -46,7 +56,7 @@ class FooterComponent implements Component {
|
|
|
46
56
|
}
|
|
47
57
|
|
|
48
58
|
private getRight(): string {
|
|
49
|
-
const statusesText = this.getStatusesText();
|
|
59
|
+
const statusesText = this.statusPosition === "inline" ? this.getStatusesText() : "";
|
|
50
60
|
const styledCostText = this.getStyledCostText();
|
|
51
61
|
const styledContextUsageText = this.getStyledContextUsageText();
|
|
52
62
|
|
|
@@ -67,8 +77,8 @@ class FooterComponent implements Component {
|
|
|
67
77
|
private getStyledCostText(): string {
|
|
68
78
|
const totalCost = this.ctx.sessionManager.getBranch().reduce((acc, entry) => acc + (getEntryUsage(entry)?.cost.total ?? 0), 0);
|
|
69
79
|
|
|
70
|
-
// Hide cost
|
|
71
|
-
if (totalCost
|
|
80
|
+
// Hide cost below half a cent, since it would render as $0.00.
|
|
81
|
+
if (totalCost < 0.005) return "";
|
|
72
82
|
|
|
73
83
|
const costText = formatCost(totalCost);
|
|
74
84
|
|
|
@@ -92,6 +102,6 @@ export function registerFooter(pi: ExtensionAPI): void {
|
|
|
92
102
|
const config = loadConfig(ctx).footer;
|
|
93
103
|
if (!ctx.hasUI || !config) return;
|
|
94
104
|
|
|
95
|
-
ctx.ui.setFooter((_tui, theme, footerData) => new FooterComponent(ctx, theme, footerData));
|
|
105
|
+
ctx.ui.setFooter((_tui, theme, footerData) => new FooterComponent(ctx, theme, footerData, config.statusPosition));
|
|
96
106
|
});
|
|
97
107
|
}
|
|
@@ -7,11 +7,53 @@ import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
|
7
7
|
export class BottomFiller implements Component {
|
|
8
8
|
private tui: TUI;
|
|
9
9
|
private measuring = false;
|
|
10
|
+
private statusContainer?: Component;
|
|
11
|
+
private originalStatusRender?: Component["render"];
|
|
12
|
+
private patchedStatusRender?: Component["render"];
|
|
10
13
|
|
|
11
14
|
constructor(tui: TUI) {
|
|
12
15
|
this.tui = tui;
|
|
13
16
|
}
|
|
14
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Suppress Pi's two-line idle status placeholder while leaving active status
|
|
20
|
+
* indicators and `clearOnShrink` unchanged.
|
|
21
|
+
*
|
|
22
|
+
* Pi added the placeholder to prevent status indicators from shrinking the TUI in
|
|
23
|
+
* [#6026](https://github.com/earendil-works/pi/pull/6026). Fullscreen already stabilizes short
|
|
24
|
+
* layouts with this filler, while in long layouts the placeholder surfaces as blank space above
|
|
25
|
+
* the editor. This render wrapper specifically counteracts that placeholder without hiding real
|
|
26
|
+
* statuses.
|
|
27
|
+
*/
|
|
28
|
+
suppressIdleStatus(): void {
|
|
29
|
+
if (this.patchedStatusRender) return;
|
|
30
|
+
|
|
31
|
+
const widgetIndex = this.tui.children.findIndex((child) => "children" in child && Array.isArray(child.children) && child.children.includes(this));
|
|
32
|
+
if (widgetIndex <= 0) return;
|
|
33
|
+
|
|
34
|
+
const statusContainer = this.tui.children[widgetIndex - 1];
|
|
35
|
+
if (!statusContainer) return;
|
|
36
|
+
|
|
37
|
+
const originalRender = statusContainer.render;
|
|
38
|
+
const patchedRender = (width: number): string[] => {
|
|
39
|
+
const lines = originalRender.call(statusContainer, width);
|
|
40
|
+
return lines.length === 2 && lines.every((line) => line === " ".repeat(width)) ? [] : lines;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
statusContainer.render = patchedRender;
|
|
44
|
+
this.statusContainer = statusContainer;
|
|
45
|
+
this.originalStatusRender = originalRender;
|
|
46
|
+
this.patchedStatusRender = patchedRender;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
dispose(): void {
|
|
50
|
+
const statusContainer = this.statusContainer;
|
|
51
|
+
const originalStatusRender = this.originalStatusRender;
|
|
52
|
+
if (statusContainer && originalStatusRender && statusContainer.render === this.patchedStatusRender) {
|
|
53
|
+
statusContainer.render = originalStatusRender;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
15
57
|
invalidate(): void {
|
|
16
58
|
// No-op
|
|
17
59
|
}
|
|
@@ -50,14 +50,17 @@ export function registerFullscreen(pi: ExtensionAPI): void {
|
|
|
50
50
|
ctx.ui.setWidget(WIDGET_KEY, (capturedTui) => {
|
|
51
51
|
tui = capturedTui;
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
const filler = new BottomFiller(capturedTui);
|
|
54
|
+
const shouldClear = pendingClear;
|
|
55
|
+
pendingClear = false;
|
|
56
|
+
|
|
57
|
+
// Defer setup until `setWidget` has mounted the filler in the render tree.
|
|
58
|
+
queueMicrotask(() => {
|
|
59
|
+
filler.suppressIdleStatus();
|
|
60
|
+
if (shouldClear) capturedTui.requestRender(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return filler;
|
|
61
64
|
});
|
|
62
65
|
});
|
|
63
66
|
|
|
@@ -76,6 +79,7 @@ export function registerFullscreen(pi: ExtensionAPI): void {
|
|
|
76
79
|
tui.terminal.write(`${line}\r\n`);
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
79
83
|
tui = undefined;
|
|
80
84
|
enabled = false;
|
|
81
85
|
pendingClear = false;
|
|
@@ -2,7 +2,8 @@ import * as z from "zod";
|
|
|
2
2
|
|
|
3
3
|
import { modelSchema } from "../../config/model";
|
|
4
4
|
|
|
5
|
+
export type PresetConfig = z.infer<typeof modelSchema>;
|
|
6
|
+
|
|
5
7
|
export const presetsConfigSchema = z.record(z.string().min(1), modelSchema);
|
|
6
8
|
|
|
7
9
|
export type PresetsConfig = z.infer<typeof presetsConfigSchema>;
|
|
8
|
-
export type PresetConfig = PresetsConfig[string];
|
|
@@ -2,7 +2,7 @@ import { PRESET_CHANGE } from "../../events";
|
|
|
2
2
|
import { formatModel } from "../../utils/format";
|
|
3
3
|
|
|
4
4
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import type {
|
|
5
|
+
import type { PresetConfig, PresetsConfig } from "./config";
|
|
6
6
|
|
|
7
7
|
export class PresetManager {
|
|
8
8
|
private pi: ExtensionAPI;
|
package/src/utils/model.ts
CHANGED
|
@@ -26,11 +26,6 @@ type ThinkingLevelSelection = {
|
|
|
26
26
|
warning: string | undefined;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
/** Check if a model has zero costs for all categories. */
|
|
30
|
-
export function isFreeModel(model: Model<Api>): boolean {
|
|
31
|
-
return model.cost.input === 0 && model.cost.output === 0 && model.cost.cacheRead === 0 && model.cost.cacheWrite === 0;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
29
|
/**
|
|
35
30
|
* Complete a one-shot background request, using an isolated session for OpenAI Codex models.
|
|
36
31
|
* See [the investigation](../../docs/background-model-calls-and-openai-codex-sessions.md).
|