pi-spark 0.10.0 → 0.10.2

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.
@@ -62,20 +62,10 @@ class FooterComponent implements Component {
62
62
  }
63
63
 
64
64
  private getStyledCostText(): string {
65
- const cost = this.ctx.sessionManager.getBranch().reduce((acc, entry) => {
66
- const entryUsage = getEntryUsage(this.ctx, entry);
67
- if (entryUsage) acc[entryUsage.type] += entryUsage.usage.cost.total;
68
- return acc;
69
- }, { subscription: 0, paid: 0 });
70
-
71
- const isSubscription = this.ctx.model ? this.ctx.modelRegistry.isUsingOAuth(this.ctx.model) : false;
72
- const subscriptionCostText = isSubscription || cost.subscription >= 0.005 ? formatCost(cost.subscription, true) : undefined;
73
- const paidCostText = !isSubscription || cost.paid >= 0.005 ? formatCost(cost.paid, false) : undefined;
74
- const costText = [subscriptionCostText, paidCostText].filter(Boolean).join(" + ");
75
-
76
- const totalCost = cost.subscription + cost.paid;
77
- if (totalCost > 20) return this.theme.fg("warning", costText);
65
+ const totalCost = this.ctx.sessionManager.getBranch().reduce((acc, entry) => acc + (getEntryUsage(entry)?.cost.total ?? 0), 0);
66
+ const costText = formatCost(totalCost);
78
67
 
68
+ if (totalCost > 20) return this.theme.fg("warning", costText);
79
69
  return this.theme.fg("dim", costText);
80
70
  }
81
71
 
@@ -16,6 +16,9 @@ const DEFAULT_THINKING_LEVEL: ModelThinkingLevel = "medium";
16
16
  /** Maximum number of models returned per list call; byte size is unrestricted. */
17
17
  const LIST_MAX_LINES = 200;
18
18
 
19
+ /** Maximum number of model rows shown when the result is collapsed. */
20
+ const COLLAPSED_MAX_ROWS = 10;
21
+
19
22
  type ModelMetadata = Omit<Model<Api>, "headers" | "compat"> & {
20
23
  thinkingLevels: ModelThinkingLevel[];
21
24
  defaultThinkingLevel: ModelThinkingLevel;
@@ -59,6 +62,16 @@ function formatPrice(value: number): string {
59
62
  return String(parseFloat(value.toFixed(2)));
60
63
  }
61
64
 
65
+ /** Notice line describing truncation or remaining pages, shared by the text result and the TUI. */
66
+ function formatListNotice(truncated: boolean, startIndex: number, endDisplay: number, total: number): string | undefined {
67
+ const nextOffset = endDisplay + 1;
68
+
69
+ if (truncated) return `[Truncated: showing models ${startIndex + 1}-${endDisplay} of ${total}. Use offset=${nextOffset} to continue.]`;
70
+ if (endDisplay < total) return `[${total - endDisplay} more models in list. Use offset=${nextOffset} to continue.]`;
71
+
72
+ return undefined;
73
+ }
74
+
62
75
  export default function (pi: ExtensionAPI) {
63
76
  pi.on("session_start", (_event, ctx) => {
64
77
  const config = loadConfig(ctx, "models");
@@ -162,15 +175,27 @@ export default function (pi: ExtensionAPI) {
162
175
  const models = details.models ?? [];
163
176
  const total = details.total ?? models.length;
164
177
 
165
- if (models.length > 0 && expanded) {
166
- addRows(models.map((model) => toModelRow(model)));
178
+ if (models.length > 0) {
179
+ const maxRows = expanded ? models.length : Math.min(models.length, COLLAPSED_MAX_ROWS);
180
+ addRows(models.slice(0, maxRows).map((model) => toModelRow(model)));
181
+
182
+ const hiddenRows = models.length - maxRows;
183
+ if (hiddenRows > 0) container.addChild(new Text(theme.fg("dim", `... (${hiddenRows} more, ${keyText("app.tools.expand")} to expand)`), 0, 0));
167
184
  container.addChild(new Spacer(1));
168
185
  }
169
186
 
170
- const summary = total > 0 ? `${models.length !== total ? `${models.length} of ` : ""}${total} ${filtered ? "matched " : ""}model${total === 1 ? "" : "s"} listed.` : `No models ${filtered ? "matched" : "found"}.`;
171
- const truncatedHint = details.truncation?.truncated ? theme.fg("warning", " (truncated)") : "";
172
- const expandHint = models.length > 0 && !expanded ? theme.fg("dim", ` (${keyText("app.tools.expand")} to expand)`) : "";
173
- container.addChild(new Text(theme.fg("muted", summary) + truncatedHint + expandHint, 0, 0));
187
+ if (details.truncation?.truncated) {
188
+ const startIndex = context.args.offset ? Math.max(0, context.args.offset - 1) : 0;
189
+ const endDisplay = startIndex + models.length;
190
+ const notice = formatListNotice(true, startIndex, endDisplay, total);
191
+ if (notice) {
192
+ container.addChild(new Text(theme.fg("warning", notice), 0, 0));
193
+ container.addChild(new Spacer(1));
194
+ }
195
+ }
196
+
197
+ const summary = `${models.length !== total ? `${models.length} of ` : ""}${total} ${filtered ? "matched " : ""}model${total === 1 ? "" : "s"} listed.`;
198
+ container.addChild(new Text(theme.fg("muted", total > 0 ? summary : `No models ${filtered ? "matched" : "found"}.`), 0, 0));
174
199
 
175
200
  return container;
176
201
  },
@@ -200,8 +225,7 @@ export default function (pi: ExtensionAPI) {
200
225
  }
201
226
  }
202
227
 
203
- const models = ctx.modelRegistry.getAll()
204
- .map((model) => toMetadata(model, ctx.modelRegistry.hasConfiguredAuth(model)));
228
+ const models = ctx.modelRegistry.getAll().map((model) => toMetadata(model, ctx.modelRegistry.hasConfiguredAuth(model)));
205
229
  const matched = listQuery ? filter(listQuery, models) : models;
206
230
  const total = matched.length;
207
231
 
@@ -229,14 +253,10 @@ export default function (pi: ExtensionAPI) {
229
253
 
230
254
  const delivered = truncation.truncated ? selected.slice(0, truncation.outputLines) : selected;
231
255
  const endDisplay = startIndex + delivered.length;
232
- const nextOffset = endDisplay + 1;
233
256
 
234
257
  let text = truncation.content;
235
- if (truncation.truncated) {
236
- text += `\n\n[Truncated: showing models ${startIndex + 1}-${endDisplay} of ${total}. Use offset=${nextOffset} to continue.]`;
237
- } else if (endDisplay < total) {
238
- text += `\n\n[${total - endDisplay} more models in list. Use offset=${nextOffset} to continue.]`;
239
- }
258
+ const notice = formatListNotice(truncation.truncated, startIndex, endDisplay, total);
259
+ if (notice) text += `\n\n${notice}`;
240
260
 
241
261
  return {
242
262
  content: [{ type: "text", text }],
@@ -24,8 +24,8 @@ export function formatContextUsage(contextUsage: ContextUsage | undefined): stri
24
24
  return `${tokens === null ? "?" : formatTokens(tokens)}/${contextWindow === null ? "?" : formatTokens(contextWindow)} (${percentText})`;
25
25
  }
26
26
 
27
- export function formatCost(cost: number, isSubscription: boolean): string {
28
- return `$${cost.toFixed(2)}${isSubscription ? " (sub)" : ""}`;
27
+ export function formatCost(cost: number): string {
28
+ return `$${cost.toFixed(2)}`;
29
29
  }
30
30
 
31
31
  export function formatCwd(cwd: string, home: string): string {
@@ -1,5 +1,5 @@
1
1
  import type { Usage } from "@earendil-works/pi-ai";
2
- import type { ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
2
+ import type { SessionEntry } from "@earendil-works/pi-coding-agent";
3
3
 
4
4
  /** Structural type guard for the pi `Usage` shape. */
5
5
  export function isUsage(value: unknown): value is Usage {
@@ -44,20 +44,17 @@ export function addUsage(a: Usage, b: Usage): Usage {
44
44
  }
45
45
 
46
46
  /**
47
- * Extract usage from a session entry, classifying it as subscription or paid.
47
+ * Extract usage from a session entry.
48
48
  *
49
- * `usage`/`provider`/`model` live in different fields depending on the entry type:
49
+ * `usage` live in different fields depending on the entry type:
50
50
  *
51
- * - `message` carries them on `.message`
52
- * - `custom` on `.data`
53
- * - `custom_message` on `.details`.
51
+ * - `message` carries them on `message`.
52
+ * - `custom` on `data`.
53
+ * - `custom_message` on `details`.
54
54
  *
55
55
  * Other entry types carry no usage. Returns `undefined` when the resolved field has no `usage`.
56
- *
57
- * An entry counts as subscription only when its `provider`/`model` resolve to an OAuth model in the
58
- * registry; everything else (including entries without `provider`/`model`) is treated as paid.
59
56
  */
60
- export function getEntryUsage(ctx: ExtensionContext, entry: SessionEntry): { type: "subscription" | "paid"; usage: Usage } | undefined {
57
+ export function getEntryUsage(entry: SessionEntry): Usage | undefined {
61
58
  let source: unknown;
62
59
  if (entry.type === "message") source = entry.message;
63
60
  else if (entry.type === "custom") source = entry.data;
@@ -65,15 +62,8 @@ export function getEntryUsage(ctx: ExtensionContext, entry: SessionEntry): { typ
65
62
  else return;
66
63
 
67
64
  if (typeof source !== "object" || source === null) return;
68
- const data = source as { usage?: unknown; provider?: unknown; model?: unknown };
65
+ const data = source as { usage?: unknown };
69
66
  if (!isUsage(data.usage)) return;
70
67
 
71
- const type = isSubscription(ctx, data.provider, data.model) ? "subscription" : "paid";
72
- return { type, usage: data.usage };
73
- }
74
-
75
- function isSubscription(ctx: ExtensionContext, provider: unknown, model: unknown): boolean {
76
- if (typeof provider !== "string" || typeof model !== "string") return false;
77
- const resolved = ctx.modelRegistry.find(provider, model);
78
- return resolved ? ctx.modelRegistry.isUsingOAuth(resolved) : false;
68
+ return data.usage;
79
69
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-spark",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "A small, opinionated collection of pi extensions",
5
5
  "keywords": [
6
6
  "pi-coding-agent",