pi-spark 0.14.6 → 0.14.7
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/package.json +1 -1
- package/src/config/index.ts +33 -24
- package/src/config/schema.ts +16 -15
- package/src/features/credits/index.ts +2 -1
- package/src/features/credits/manager.ts +9 -5
- package/src/features/editor/index.ts +1 -1
- package/src/features/footer/index.ts +3 -3
- package/src/features/pi/actions/models.ts +2 -2
- package/src/features/presets/selector.ts +6 -6
- package/src/features/web/render.ts +2 -2
package/package.json
CHANGED
package/src/config/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { featureSchemas } from "./schema";
|
|
6
6
|
|
|
7
7
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import type { SparkConfig } from "./schema";
|
|
@@ -10,18 +10,6 @@ import type { SparkConfig } from "./schema";
|
|
|
10
10
|
type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
|
|
11
11
|
type JsonObject = { [key: string]: JsonValue };
|
|
12
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
13
|
const cache = new Map<string, SparkConfig>();
|
|
26
14
|
|
|
27
15
|
/** Load and validate spark.json once per session lifecycle; later calls return the cached result. */
|
|
@@ -30,23 +18,44 @@ export function loadConfig(ctx: ExtensionContext, fileName: string = "spark.json
|
|
|
30
18
|
const cached = cache.get(key);
|
|
31
19
|
if (cached) return cached;
|
|
32
20
|
|
|
33
|
-
const
|
|
34
|
-
const
|
|
21
|
+
const rawValue = loadMergedJson(getConfigPaths(ctx.cwd, fileName)) ?? {};
|
|
22
|
+
const raw = isPlainObject(rawValue) ? rawValue : {};
|
|
23
|
+
|
|
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[] = [];
|
|
28
|
+
|
|
29
|
+
for (const field of Object.keys(featureSchemas) as (keyof SparkConfig)[]) {
|
|
30
|
+
const value = raw[field];
|
|
31
|
+
|
|
32
|
+
if (value === undefined) {
|
|
33
|
+
config[field] = {};
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
if (value === false) {
|
|
38
|
+
config[field] = false;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
39
41
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
+
const result = featureSchemas[field].safeParse(value);
|
|
43
|
+
if (result.success) {
|
|
44
|
+
config[field] = result.data;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
config[field] = {};
|
|
49
|
+
const detail = result.error.issues.map((issue) => `${[field, ...issue.path].join(".")}: ${issue.message}`).join("; ");
|
|
50
|
+
errors.push(detail);
|
|
44
51
|
}
|
|
45
52
|
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
if (errors.length > 0) {
|
|
54
|
+
ctx.ui.notify(`Invalid spark config, using defaults for: ${errors.join("; ")}`, "error");
|
|
55
|
+
}
|
|
48
56
|
|
|
49
|
-
|
|
57
|
+
cache.set(key, config as SparkConfig);
|
|
58
|
+
return config as SparkConfig;
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
function getConfigPaths(cwd: string, fileName: string): [globalPath: string, projectPath: string] {
|
package/src/config/schema.ts
CHANGED
|
@@ -9,22 +9,23 @@ import { presetsConfigSchema } from "../features/presets/config";
|
|
|
9
9
|
import { recapConfigSchema } from "../features/recap/config";
|
|
10
10
|
import { webConfigSchema } from "../features/web/config";
|
|
11
11
|
|
|
12
|
-
const disabled = z.literal(false);
|
|
13
|
-
|
|
14
12
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* Raw option shape for each feature. The enable/disable/default policy lives in `loadConfig`:
|
|
14
|
+
* an omitted field falls back to `{}` (enabled with defaults), `false` disables the feature, and
|
|
15
|
+
* any other value is validated against the feature schema.
|
|
17
16
|
*/
|
|
18
|
-
export const
|
|
19
|
-
credits: creditsConfigSchema
|
|
20
|
-
editor: editorConfigSchema
|
|
21
|
-
footer: footerConfigSchema
|
|
22
|
-
fullscreen: fullscreenConfigSchema
|
|
23
|
-
pi: piConfigSchema
|
|
24
|
-
presets: presetsConfigSchema
|
|
25
|
-
recap: recapConfigSchema
|
|
26
|
-
web: webConfigSchema
|
|
27
|
-
}
|
|
17
|
+
export const featureSchemas = {
|
|
18
|
+
credits: creditsConfigSchema,
|
|
19
|
+
editor: editorConfigSchema,
|
|
20
|
+
footer: footerConfigSchema,
|
|
21
|
+
fullscreen: fullscreenConfigSchema,
|
|
22
|
+
pi: piConfigSchema,
|
|
23
|
+
presets: presetsConfigSchema,
|
|
24
|
+
recap: recapConfigSchema,
|
|
25
|
+
web: webConfigSchema,
|
|
26
|
+
} as const;
|
|
28
27
|
|
|
29
28
|
/** Resolved config for every feature; `false` means the feature is disabled. */
|
|
30
|
-
export type SparkConfig =
|
|
29
|
+
export type SparkConfig = {
|
|
30
|
+
[K in keyof typeof featureSchemas]: z.infer<(typeof featureSchemas)[K]> | false;
|
|
31
|
+
};
|
|
@@ -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);
|
|
@@ -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
|
|
|
@@ -39,7 +39,7 @@ class FooterComponent implements Component {
|
|
|
39
39
|
const branch = this.footerData.getGitBranch();
|
|
40
40
|
const sessionName = this.ctx.sessionManager.getSessionName();
|
|
41
41
|
|
|
42
|
-
return this.theme.fg("dim", [cwdText, branch, sessionName].filter(Boolean).join("
|
|
42
|
+
return this.theme.fg("dim", [cwdText, branch, sessionName].filter(Boolean).join(" · "));
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
private getRight(): string {
|
|
@@ -47,7 +47,7 @@ class FooterComponent implements Component {
|
|
|
47
47
|
const styledCostText = this.getStyledCostText();
|
|
48
48
|
const styledContextUsageText = this.getStyledContextUsageText();
|
|
49
49
|
|
|
50
|
-
return [statusesText, styledCostText, styledContextUsageText].filter(Boolean).join(this.theme.fg("dim", "
|
|
50
|
+
return [statusesText, styledCostText, styledContextUsageText].filter(Boolean).join(this.theme.fg("dim", " · "));
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/** Get extension statuses, sorted by key alphabetically. */
|
|
@@ -58,7 +58,7 @@ class FooterComponent implements Component {
|
|
|
58
58
|
return Array.from(extensionStatuses.entries())
|
|
59
59
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
60
60
|
.map(([, text]) => sanitizeText(text))
|
|
61
|
-
.join(this.theme.fg("dim", "
|
|
61
|
+
.join(this.theme.fg("dim", " · "));
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
private getStyledCostText(): string {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { keyHint, truncateHead } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { filter, parse } from "liqe";
|
|
4
4
|
import { Type } from "typebox";
|
|
@@ -92,7 +92,7 @@ export const modelsAction = defineAction({
|
|
|
92
92
|
addRows(models.slice(0, maxRows).map((model) => toModelRow(model)));
|
|
93
93
|
|
|
94
94
|
const hiddenRows = models.length - maxRows;
|
|
95
|
-
if (hiddenRows > 0) container.addChild(new Text(theme.fg("dim", `... (${hiddenRows} more,
|
|
95
|
+
if (hiddenRows > 0) container.addChild(new Text(theme.fg("dim", `... (${hiddenRows} more, `) + keyHint("app.tools.expand", "to expand") + theme.fg("dim", ")"), 0, 0));
|
|
96
96
|
container.addChild(new Spacer(1));
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -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)));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
3
3
|
|
|
4
4
|
import { joinTextContent } from "../../utils/format";
|
|
@@ -28,7 +28,7 @@ export function renderWebResult(result: AgentToolResult<unknown>, expanded: bool
|
|
|
28
28
|
container.addChild(new Text(theme.fg("muted", lines.slice(0, maxLines).join("\n")), 0, 0));
|
|
29
29
|
|
|
30
30
|
const hidden = lines.length - maxLines;
|
|
31
|
-
if (hidden > 0) container.addChild(new Text(theme.fg("dim", `... (${hidden} more lines,
|
|
31
|
+
if (hidden > 0) container.addChild(new Text(theme.fg("dim", `... (${hidden} more lines, `) + keyHint("app.tools.expand", "to expand") + theme.fg("dim", ")"), 0, 0));
|
|
32
32
|
|
|
33
33
|
return container;
|
|
34
34
|
}
|