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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-spark",
3
- "version": "0.14.6",
3
+ "version": "0.14.7",
4
4
  "description": "Pi package that polishes your daily experience and keeps you at the frontier of agentic workflows.",
5
5
  "keywords": [
6
6
  "pi-coding-agent",
@@ -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 { sparkConfigSchema } from "./schema";
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 rawConfig = loadMergedJson(getConfigPaths(ctx.cwd, fileName)) ?? {};
34
- const result = sparkConfigSchema.safeParse(rawConfig);
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
- if (!result.success) {
37
- const message = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ");
38
- ctx.ui.notify(`Invalid spark config: ${message}`, "error");
37
+ if (value === false) {
38
+ config[field] = false;
39
+ continue;
40
+ }
39
41
 
40
- const config = DISABLED_CONFIG;
41
- cache.set(key, config);
42
+ const result = featureSchemas[field].safeParse(value);
43
+ if (result.success) {
44
+ config[field] = result.data;
45
+ continue;
46
+ }
42
47
 
43
- return config;
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
- const config = result.data;
47
- cache.set(key, config);
53
+ if (errors.length > 0) {
54
+ ctx.ui.notify(`Invalid spark config, using defaults for: ${errors.join("; ")}`, "error");
55
+ }
48
56
 
49
- return config;
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] {
@@ -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
- * Each feature field is `{ ... } | false`: `false` disables the feature, an object configures
16
- * it, and an omitted field falls back to defaults (`{}`, enabled).
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 sparkConfigSchema = z.object({
19
- credits: creditsConfigSchema.or(disabled).default({}),
20
- editor: editorConfigSchema.or(disabled).default({}),
21
- footer: footerConfigSchema.or(disabled).default({}),
22
- fullscreen: fullscreenConfigSchema.or(disabled).default({}),
23
- pi: piConfigSchema.or(disabled).default({}),
24
- presets: presetsConfigSchema.or(disabled).default({}),
25
- recap: recapConfigSchema.or(disabled).default({}),
26
- web: webConfigSchema.or(disabled).default({}),
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 = z.infer<typeof sparkConfigSchema>;
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
- if (ctx.signal) signals.push(ctx.signal);
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 || ctx.signal?.aborted) return;
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 { keyText, truncateHead } from "@earendil-works/pi-coding-agent";
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, ${keyText("app.tools.expand")} to expand)`), 0, 0));
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 { keyText, DynamicBorder } from "@earendil-works/pi-coding-agent";
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
- ["↑↓", "navigate"],
43
- [keyText("tui.select.confirm"), "select"],
44
- [keyText("tui.select.cancel"), "cancel"],
45
- ] as const;
46
- box.addChild(new Text(keyHints.map((item) => `${theme.fg("dim", item[0])} ${theme.fg("muted", item[1])}`).join(" "), 0, 0));
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 { keyText } from "@earendil-works/pi-coding-agent";
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, ${keyText("app.tools.expand")} to expand)`), 0, 0));
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
  }