pi-spark 0.23.1 → 0.24.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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.24.0](https://github.com/zlliang/pi-spark/compare/v0.23.1...v0.24.0) (2026-09-14)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * adopt Pi 0.85 extension APIs
9
+
10
+ ### Features
11
+
12
+ * adopt Pi 0.85 extension APIs ([2cd3208](https://github.com/zlliang/pi-spark/commit/2cd3208fe5bf8aade7828720ae0397da6836fe99))
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * capitalize DeepSeek pricing labels ([fffcc40](https://github.com/zlliang/pi-spark/commit/fffcc40c8664056fb54d3c5fea5853f3805a2e4f))
18
+
3
19
  ## [0.23.1](https://github.com/zlliang/pi-spark/compare/v0.23.0...v0.23.1) (2026-09-12)
4
20
 
5
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-spark",
3
- "version": "0.23.1",
3
+ "version": "0.24.0",
4
4
  "description": "Pi package that polishes your daily experience",
5
5
  "keywords": [
6
6
  "pi-coding-agent",
@@ -44,17 +44,17 @@
44
44
  "zod": "^4.4.3"
45
45
  },
46
46
  "peerDependencies": {
47
- "@earendil-works/pi-agent-core": ">=0.84.0",
48
- "@earendil-works/pi-ai": ">=0.84.0",
49
- "@earendil-works/pi-coding-agent": ">=0.84.0",
50
- "@earendil-works/pi-tui": ">=0.84.0",
47
+ "@earendil-works/pi-agent-core": ">=0.85.0",
48
+ "@earendil-works/pi-ai": ">=0.85.0",
49
+ "@earendil-works/pi-coding-agent": ">=0.85.0",
50
+ "@earendil-works/pi-tui": ">=0.85.0",
51
51
  "typebox": "*"
52
52
  },
53
53
  "devDependencies": {
54
- "@earendil-works/pi-agent-core": ">=0.84.0",
55
- "@earendil-works/pi-ai": ">=0.84.0",
56
- "@earendil-works/pi-coding-agent": ">=0.84.0",
57
- "@earendil-works/pi-tui": ">=0.84.0",
54
+ "@earendil-works/pi-agent-core": ">=0.85.0",
55
+ "@earendil-works/pi-ai": ">=0.85.0",
56
+ "@earendil-works/pi-coding-agent": ">=0.85.0",
57
+ "@earendil-works/pi-tui": ">=0.85.0",
58
58
  "@types/node": "*",
59
59
  "typebox": "*",
60
60
  "typescript": "^7.0.2"
@@ -50,6 +50,10 @@ export function registerCredits(pi: ExtensionAPI): void {
50
50
  creditsManager?.refresh(ctx);
51
51
  });
52
52
 
53
+ pi.on("session_compact_failed", (_event, ctx) => {
54
+ creditsManager?.refresh(ctx);
55
+ });
56
+
53
57
  pi.on("session_before_tree", (event, ctx) => {
54
58
  if (!event.preparation.userWantsSummary) return;
55
59
 
@@ -25,34 +25,34 @@ export const deepseekProvider: CreditsProvider = {
25
25
  pricingRules: [
26
26
  {
27
27
  model: "deepseek-flash",
28
- defaultCost: { input: 0.15, output: 0.6, cacheRead: 0.003, cacheWrite: 0, label: "off-peak" },
28
+ defaultCost: { input: 0.15, output: 0.6, cacheRead: 0.003, cacheWrite: 0, label: "Off-peak" },
29
29
  windows: [
30
- { start: "01:00", end: "04:00", days: [1, 2, 3, 4, 5], cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, label: "peak" } },
31
- { start: "06:00", end: "10:00", days: [1, 2, 3, 4, 5], cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, label: "peak" } },
30
+ { start: "01:00", end: "04:00", days: [1, 2, 3, 4, 5], cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, label: "Peak" } },
31
+ { start: "06:00", end: "10:00", days: [1, 2, 3, 4, 5], cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, label: "Peak" } },
32
32
  ],
33
33
  },
34
34
  {
35
35
  model: "deepseek-v4-flash",
36
- defaultCost: { input: 0.15, output: 0.6, cacheRead: 0.003, cacheWrite: 0, label: "off-peak" },
36
+ defaultCost: { input: 0.15, output: 0.6, cacheRead: 0.003, cacheWrite: 0, label: "Off-peak" },
37
37
  windows: [
38
- { start: "01:00", end: "04:00", days: [1, 2, 3, 4, 5], cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, label: "peak" } },
39
- { start: "06:00", end: "10:00", days: [1, 2, 3, 4, 5], cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, label: "peak" } },
38
+ { start: "01:00", end: "04:00", days: [1, 2, 3, 4, 5], cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, label: "Peak" } },
39
+ { start: "06:00", end: "10:00", days: [1, 2, 3, 4, 5], cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, label: "Peak" } },
40
40
  ],
41
41
  },
42
42
  {
43
43
  model: "deepseek-v4-flash-vision-exp",
44
- defaultCost: { input: 0.15, output: 0.6, cacheRead: 0.003, cacheWrite: 0, label: "off-peak" },
44
+ defaultCost: { input: 0.15, output: 0.6, cacheRead: 0.003, cacheWrite: 0, label: "Off-peak" },
45
45
  windows: [
46
- { start: "01:00", end: "04:00", days: [1, 2, 3, 4, 5], cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, label: "peak" } },
47
- { start: "06:00", end: "10:00", days: [1, 2, 3, 4, 5], cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, label: "peak" } },
46
+ { start: "01:00", end: "04:00", days: [1, 2, 3, 4, 5], cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, label: "Peak" } },
47
+ { start: "06:00", end: "10:00", days: [1, 2, 3, 4, 5], cost: { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, label: "Peak" } },
48
48
  ],
49
49
  },
50
50
  {
51
51
  model: "deepseek-v4-pro",
52
- defaultCost: { input: 0.66, output: 1.98, cacheRead: 0.022, cacheWrite: 0, label: "off-peak" },
52
+ defaultCost: { input: 0.66, output: 1.98, cacheRead: 0.022, cacheWrite: 0, label: "Off-peak" },
53
53
  windows: [
54
- { start: "01:00", end: "04:00", days: [1, 2, 3, 4, 5], cost: { input: 1.32, output: 3.96, cacheRead: 0.044, cacheWrite: 0, label: "peak" } },
55
- { start: "06:00", end: "10:00", days: [1, 2, 3, 4, 5], cost: { input: 1.32, output: 3.96, cacheRead: 0.044, cacheWrite: 0, label: "peak" } },
54
+ { start: "01:00", end: "04:00", days: [1, 2, 3, 4, 5], cost: { input: 1.32, output: 3.96, cacheRead: 0.044, cacheWrite: 0, label: "Peak" } },
55
+ { start: "06:00", end: "10:00", days: [1, 2, 3, 4, 5], cost: { input: 1.32, output: 3.96, cacheRead: 0.044, cacheWrite: 0, label: "Peak" } },
56
56
  ],
57
57
  },
58
58
  ],
@@ -5,7 +5,7 @@ import { Loader } from "../../../components/loader";
5
5
  import { renderCredits } from "../status";
6
6
 
7
7
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
8
- import type { SelectItem } from "@earendil-works/pi-tui";
8
+ import type { SelectItem, TuiMouseEvent } from "@earendil-works/pi-tui";
9
9
  import type { BankedRateLimitReset, RateLimitResetCreditsResponse } from "./openai-codex";
10
10
  import type { Credits } from "../types";
11
11
 
@@ -113,6 +113,7 @@ export async function showCodexResetsSelector(ctx: ExtensionContext, load: (sign
113
113
  finish(null);
114
114
  }
115
115
  },
116
+ handleMouse: (event: TuiMouseEvent) => container.handleMouse(event),
116
117
  };
117
118
  });
118
119
 
@@ -170,6 +171,7 @@ export async function confirmCodexReset(ctx: ExtensionContext, credit: BankedRat
170
171
  selectList.handleInput(data);
171
172
  tui.requestRender();
172
173
  },
174
+ handleMouse: (event: TuiMouseEvent) => container.handleMouse(event),
173
175
  };
174
176
  });
175
177
 
@@ -1,6 +1,7 @@
1
- import { hyperlink } from "@earendil-works/pi-tui";
2
1
  import prettyMilliseconds from "pretty-ms";
3
2
 
3
+ import { formatLink } from "../../utils/format";
4
+
4
5
  import type { Theme } from "@earendil-works/pi-coding-agent";
5
6
  import type { Credits, CreditsLane } from "./types";
6
7
 
@@ -15,7 +16,7 @@ export function renderCredits(theme: Theme, label: string, credits: Credits, lin
15
16
  const suffix = credits.suffix ? ` ${theme.fg("dim", credits.suffix)}` : "";
16
17
 
17
18
  const text = `${styledLabel}${value}${suffix}`;
18
- return link ? hyperlink(text, link) : text;
19
+ return link ? formatLink(text, link) : text;
19
20
  }
20
21
 
21
22
  export function renderError(theme: Theme, label: string, message: string): string {
@@ -43,27 +43,28 @@ class Editor extends CustomEditor {
43
43
  this.tui.requestRender();
44
44
  }
45
45
 
46
- render(width: number): string[] {
46
+ override render(width: number): string[] {
47
47
  // Pi reapplies the thinking-level border color when the level changes. Use the dim color here
48
- // while preserving Bash mode's dedicated border color.
48
+ // while preserving Bash mode's dedicated border color. Set it before rendering, since the
49
+ // border helpers below run inside `super.render()`.
49
50
  if (this.thinkingLevelIndicator === "model" && !this.getText().trimStart().startsWith("!")) {
50
51
  this.borderColor = (text) => this.ctx.ui.theme.fg("dim", text);
51
52
  }
52
53
 
53
- const lines = super.render(width);
54
- if (lines.length === 0) return lines;
54
+ return super.render(width);
55
+ }
55
56
 
56
- lines[0] = this.getTopBorder(width);
57
+ protected override renderTopBorder(width: number, hiddenLineCount: number): string {
58
+ return this.renderBorder(width, this.getTopLeft(hiddenLineCount), this.getTopRight());
59
+ }
57
60
 
58
- return lines;
61
+ protected override renderBottomBorder(width: number, hiddenLineCount: number): string {
62
+ return this.renderBorder(width, this.getScrollHint("↓", hiddenLineCount), "");
59
63
  }
60
64
 
61
- private getTopBorder(width: number): string {
65
+ private renderBorder(width: number, left: string, right: string): string {
62
66
  const theme = this.ctx.ui.theme;
63
67
 
64
- const left = this.getLeft();
65
- const right = this.getRight();
66
-
67
68
  return new SplitLine(left, right, {
68
69
  padding: 1,
69
70
  innerPadding: 1,
@@ -72,16 +73,17 @@ class Editor extends CustomEditor {
72
73
  }).render(width)[0];
73
74
  }
74
75
 
75
- private getLeft(): string {
76
+ private getTopLeft(hiddenLineCount: number): string {
76
77
  const theme = this.ctx.ui.theme;
77
78
 
78
79
  const spinner = this.spinner.getFrame();
79
80
  const workingMessage = this.workingMessage;
81
+ const workingText = [spinner ? theme.fg("accent", spinner) : undefined, workingMessage ? theme.fg("dim", workingMessage) : undefined].filter(Boolean).join(" ");
80
82
 
81
- return [theme.fg("accent", spinner), workingMessage ? theme.fg("dim", workingMessage) : undefined].filter(Boolean).join(" ");
83
+ return [this.getScrollHint("↑", hiddenLineCount), workingText].filter(Boolean).join(theme.fg("dim", " · "));
82
84
  }
83
85
 
84
- private getRight(): string {
86
+ private getTopRight(): string {
85
87
  const theme = this.ctx.ui.theme;
86
88
 
87
89
  const modelBeforeText = this.slots.modelBefore;
@@ -91,6 +93,13 @@ class Editor extends CustomEditor {
91
93
  return [modelBeforeText ? theme.fg("dim", modelBeforeText) : undefined, coloredModelText].filter(Boolean).join(theme.fg("dim", " · "));
92
94
  }
93
95
 
96
+ /** Renders Pi's hidden-line hint, which this editor keeps on the left side of the border. */
97
+ private getScrollHint(direction: "↑" | "↓", hiddenLineCount: number): string {
98
+ if (hiddenLineCount <= 0) return "";
99
+
100
+ return this.ctx.ui.theme.fg("dim", `${direction} ${hiddenLineCount} more`);
101
+ }
102
+
94
103
  private withThinkingLevelColor(text: string): string {
95
104
  const theme = this.ctx.ui.theme;
96
105
  const thinkingLevel = this.pi.getThinkingLevel();
@@ -1,11 +1,11 @@
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
+ import { Text } from "@earendil-works/pi-tui";
5
5
 
6
6
  import { SplitLine } from "../../components/split-line";
7
7
  import { loadConfig } from "../../config";
8
- import { formatContextUsage, formatCwd, sanitizeText } from "../../utils/format";
8
+ import { formatContextUsage, formatCwd, formatLink, sanitizeText } from "../../utils/format";
9
9
  import { getEntryUsage } from "../../utils/usage";
10
10
 
11
11
  import type { ExtensionContext, ExtensionAPI, ReadonlyFooterDataProvider, Theme } from "@earendil-works/pi-coding-agent";
@@ -48,7 +48,7 @@ class FooterComponent implements Component {
48
48
  private getLeft(): string {
49
49
  const cwd = this.ctx.sessionManager.getCwd();
50
50
  const url = pathToFileURL(resolve(cwd));
51
- const cwdText = hyperlink(formatCwd(cwd, homedir()), url.href);
51
+ const cwdText = formatLink(formatCwd(cwd, homedir()), url.href);
52
52
  const branch = this.footerData.getGitBranch();
53
53
  const sessionName = this.ctx.sessionManager.getSessionName();
54
54
 
@@ -2,6 +2,7 @@ import { keyHint, rawKeyHint, DynamicBorder } from "@earendil-works/pi-coding-ag
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";
5
+ import type { TuiMouseEvent } from "@earendil-works/pi-tui";
5
6
  import type { PresetManager } from "./manager";
6
7
 
7
8
  export async function showPresetSelector(ctx: ExtensionContext, presetManager: PresetManager): Promise<string | undefined> {
@@ -51,6 +52,7 @@ export async function showPresetSelector(ctx: ExtensionContext, presetManager: P
51
52
  selectList.handleInput(data);
52
53
  tui.requestRender();
53
54
  },
55
+ handleMouse: (event: TuiMouseEvent) => container.handleMouse(event),
54
56
  };
55
57
  });
56
58
 
@@ -56,6 +56,10 @@ export function registerRecap(pi: ExtensionAPI): void {
56
56
  idleListener?.watch(ctx);
57
57
  });
58
58
 
59
+ pi.on("session_compact_failed", (_event, ctx) => {
60
+ idleListener?.watch(ctx);
61
+ });
62
+
59
63
  pi.on("session_tree", (_event, ctx) => {
60
64
  idleListener?.watch(ctx);
61
65
  });
@@ -139,29 +139,18 @@ function formatWriteCall(path: string | null, content: string | null, highlighte
139
139
 
140
140
  export function registerWrite(pi: ExtensionAPI): void {
141
141
  let enabled = true;
142
- const toolCache = new Map<string, ReturnType<typeof createWriteToolDefinition>>();
143
142
 
144
- function getWriteTool(cwd: string): ReturnType<typeof createWriteToolDefinition> {
145
- let tool = toolCache.get(cwd);
146
- if (!tool) {
147
- tool = createWriteToolDefinition(cwd);
148
- toolCache.set(cwd, tool);
149
- }
150
-
151
- return tool;
152
- };
143
+ // Pi resolves paths against `ctx.cwd`, falling back to this cwd only when neither is provided.
144
+ const writeTool = createWriteToolDefinition(process.cwd());
153
145
 
154
146
  pi.on("session_start", (_event, ctx) => {
155
147
  enabled = loadConfig(ctx).write !== false;
156
148
  });
157
149
 
158
150
  pi.registerTool({
159
- ...getWriteTool(process.cwd()),
160
- execute(toolCallId, params, signal, onUpdate, ctx) {
161
- return getWriteTool(ctx.cwd).execute(toolCallId, params, signal, onUpdate, ctx);
162
- },
151
+ ...writeTool,
163
152
  renderCall(args, theme, context) {
164
- if (!enabled) return getWriteTool(context.cwd).renderCall!(args, theme, context);
153
+ if (!enabled) return writeTool.renderCall!(args, theme, context);
165
154
 
166
155
  const renderArgs = args as { path?: string; file_path?: string; content?: string } | undefined;
167
156
  const path = parseArg(renderArgs?.file_path ?? renderArgs?.path);
@@ -1,7 +1,8 @@
1
1
  import { homedir } from "node:os";
2
2
  import { isAbsolute, join, resolve } from "node:path";
3
3
  import { fileURLToPath, pathToFileURL } from "node:url";
4
- import { getCapabilities, hyperlink } from "@earendil-works/pi-tui";
4
+
5
+ import { formatLink } from "../../utils/format";
5
6
 
6
7
  import type { Theme } from "@earendil-works/pi-coding-agent";
7
8
 
@@ -12,9 +13,8 @@ export function renderPath(path: string | null, cwd: string, theme: Theme): stri
12
13
  const home = homedir();
13
14
  const displayPath = path.startsWith(home) ? `~${path.slice(home.length)}` : path;
14
15
  const styledPath = theme.fg("accent", displayPath);
15
- if (!getCapabilities().hyperlinks) return styledPath;
16
16
 
17
- return hyperlink(styledPath, pathToFileURL(resolvePath(path, cwd)).href);
17
+ return formatLink(styledPath, pathToFileURL(resolvePath(path, cwd)).href);
18
18
  }
19
19
 
20
20
  function resolvePath(path: string, cwd: string): string {
@@ -1,10 +1,16 @@
1
1
  import { isAbsolute, relative, resolve, sep } from "node:path";
2
+ import { getCapabilities, hyperlink } from "@earendil-works/pi-tui";
2
3
 
3
4
  import { http } from "./http";
4
5
 
5
6
  import type { ProviderId } from "@earendil-works/pi-ai";
6
7
  import type { ContextUsage } from "@earendil-works/pi-coding-agent";
7
8
 
9
+ /** Links text with OSC 8, honoring the terminal's detected and user-overridden hyperlink support. */
10
+ export function formatLink(text: string, url: string): string {
11
+ return getCapabilities().hyperlinks ? hyperlink(text, url) : text;
12
+ }
13
+
8
14
  export function formatModel(provider?: ProviderId, model?: string, thinkingLevel?: string): string {
9
15
  return provider && model ? `${provider}/${model}${thinkingLevel ? `:${thinkingLevel}` : ""}` : "no-model";
10
16
  }