@janvitos/pi-plan-build 0.1.8 → 0.1.9

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 CHANGED
@@ -8,7 +8,7 @@ A global [Pi coding agent](https://github.com/badlogic/pi-mono) extension that a
8
8
 
9
9
  - New sessions start in **Build** mode.
10
10
  - Bare `Tab` cycles **Build → Plan → Build**, while active autocomplete dropdowns retain Pi's normal Tab completion.
11
- - The composer uses OpenCode prompt-inspired blue/orange mode colors on a rounded `╭─` top border and `│` rail, plus a mode/thinking metadata row and Pi's bottom border.
11
+ - The composer uses OpenCode prompt-inspired blue/orange mode colors on a rounded `╭─` top border and `│` rail, plus a mode/model/thinking metadata row and Pi's bottom border. The footer keeps the remaining path and usage stats without duplicating model metadata.
12
12
  - `/plan`, `/build`, and the `--plan` startup flag.
13
13
  - Per-session plans at `~/.pi/agent/plans/<session-id>.md`.
14
14
  - In Plan mode, built-in `edit` and `write` are restricted to the exact plan file.
package/index.ts CHANGED
@@ -20,9 +20,11 @@ import {
20
20
  classifyPlanExitChoice,
21
21
  decodeModeState,
22
22
  extractPromptHistory,
23
+ formatFooterCwd,
23
24
  formatModeMetadata,
24
25
  formatModeRail,
25
26
  formatModeTopBorder,
27
+ formatTokens,
26
28
  isAllowedPlanMutation,
27
29
  makePlanPath,
28
30
  nextMode,
@@ -393,6 +395,84 @@ export default function planBuildModes(pi: ExtensionAPI): void {
393
395
  updateModeIndicator(ctx);
394
396
 
395
397
  if (ctx.mode === "tui") {
398
+ ctx.ui.setFooter((tui, theme, footerData) => {
399
+ const unsubscribe = footerData.onBranchChange(() => tui.requestRender());
400
+ return {
401
+ dispose: unsubscribe,
402
+ invalidate() {},
403
+ render(width: number): string[] {
404
+ let input = 0;
405
+ let output = 0;
406
+ let cacheRead = 0;
407
+ let cacheWrite = 0;
408
+ let cost = 0;
409
+ let latestCacheHitRate: number | undefined;
410
+ for (const entry of ctx.sessionManager.getEntries()) {
411
+ const usage =
412
+ entry.type === "message" &&
413
+ (entry.message.role === "assistant" || entry.message.role === "toolResult")
414
+ ? entry.message.usage
415
+ : (entry.type === "branch_summary" || entry.type === "compaction")
416
+ ? entry.usage
417
+ : undefined;
418
+ if (!usage) continue;
419
+ input += usage.input;
420
+ output += usage.output;
421
+ cacheRead += usage.cacheRead;
422
+ cacheWrite += usage.cacheWrite;
423
+ cost += usage.cost.total;
424
+ if (entry.type === "message" && entry.message.role === "assistant") {
425
+ const promptTokens = usage.input + usage.cacheRead + usage.cacheWrite;
426
+ latestCacheHitRate = promptTokens > 0 ? (usage.cacheRead / promptTokens) * 100 : undefined;
427
+ }
428
+ }
429
+
430
+ let cwd = formatFooterCwd(ctx.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);
431
+ const branch = footerData.getGitBranch();
432
+ if (branch) cwd += ` (${branch})`;
433
+ const sessionName = ctx.sessionManager.getSessionName();
434
+ if (sessionName) cwd += ` • ${sessionName}`;
435
+
436
+ const stats: string[] = [];
437
+ if (input) stats.push(`↑${formatTokens(input)}`);
438
+ if (output) stats.push(`↓${formatTokens(output)}`);
439
+ if (cacheRead) stats.push(`R${formatTokens(cacheRead)}`);
440
+ if (cacheWrite) stats.push(`W${formatTokens(cacheWrite)}`);
441
+ if ((cacheRead || cacheWrite) && latestCacheHitRate !== undefined) {
442
+ stats.push(`CH${latestCacheHitRate.toFixed(1)}%`);
443
+ }
444
+ const usingSubscription = ctx.model
445
+ ? ctx.model.provider === "kimi-coding" || ctx.modelRegistry.isUsingOAuth(ctx.model)
446
+ : false;
447
+ if (cost || usingSubscription) {
448
+ stats.push(`$${cost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`);
449
+ }
450
+
451
+ const contextUsage = ctx.getContextUsage();
452
+ const contextWindow = contextUsage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
453
+ const contextPercent = contextUsage?.percent;
454
+ const contextText = `${contextPercent === null || contextPercent === undefined ? "?" : `${contextPercent.toFixed(1)}%`}/${formatTokens(contextWindow)} (auto)`;
455
+ stats.push(
456
+ contextPercent !== null && contextPercent !== undefined && contextPercent > 90
457
+ ? theme.fg("error", contextText)
458
+ : contextPercent !== null && contextPercent !== undefined && contextPercent > 70
459
+ ? theme.fg("warning", contextText)
460
+ : contextText,
461
+ );
462
+
463
+ const lines = [
464
+ truncateToWidth(theme.fg("dim", cwd), width, theme.fg("dim", "...")),
465
+ truncateToWidth(theme.fg("dim", stats.join(" ")), width, theme.fg("dim", "...")),
466
+ ];
467
+ const statuses = Array.from(footerData.getExtensionStatuses().entries())
468
+ .sort(([a], [b]) => a.localeCompare(b))
469
+ .map(([, text]) => text.replace(/[\r\n\t]/g, " ").replace(/ +/g, " ").trim());
470
+ if (statuses.length) lines.push(truncateToWidth(statuses.join(" "), width, theme.fg("dim", "...")));
471
+ return lines;
472
+ },
473
+ };
474
+ });
475
+
396
476
  // Startup history is populated after session_start; replacement flows recreate the editor after that step.
397
477
  const promptHistory = event.reason === "startup" ? [] : extractPromptHistory(ctx.sessionManager.getBranch());
398
478
  class ModeEditor extends CustomEditor {
@@ -412,7 +492,10 @@ export default function planBuildModes(pi: ExtensionAPI): void {
412
492
 
413
493
  const rail = `${formatModeRail(selectedMode)} `;
414
494
  const metadata = truncateToWidth(
415
- formatModeMetadata(selectedMode, pi.getThinkingLevel(), ctx.ui.theme, this.borderColor),
495
+ formatModeMetadata(selectedMode, pi.getThinkingLevel(), ctx.ui.theme, this.borderColor, {
496
+ modelName: ctx.model?.id ?? "no-model",
497
+ modelColor: (text) => ctx.ui.theme.fg("dim", text),
498
+ }),
416
499
  width,
417
500
  "",
418
501
  );
@@ -442,6 +525,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
442
525
 
443
526
  pi.on("session_shutdown", async (_event, ctx) => {
444
527
  ctx.ui.setStatus(STATUS_KEY, undefined);
528
+ ctx.ui.setFooter(undefined);
445
529
  ctx.ui.setEditorComponent(undefined);
446
530
  requestEditorRender = undefined;
447
531
  currentContext = undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@janvitos/pi-plan-build",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Plan safely, approve explicitly, then implement here or in a clean session.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/utils.ts CHANGED
@@ -1,4 +1,4 @@
1
- import path from "node:path";
1
+ import path, { isAbsolute, relative, resolve, sep } from "node:path";
2
2
 
3
3
  export type Mode = "build" | "plan";
4
4
 
@@ -13,6 +13,11 @@ export interface ModeStatusTheme {
13
13
  fg(color: "dim", text: string): string;
14
14
  }
15
15
 
16
+ export interface PromptMetadataOptions {
17
+ modelName: string;
18
+ modelColor: (text: string) => string;
19
+ }
20
+
16
21
  function formatModeColor(mode: Mode, text: string): string {
17
22
  return `\x1b[${MODE_LABELS[mode].color}m${text}${ANSI_RESET}`;
18
23
  }
@@ -31,10 +36,34 @@ export function formatModeMetadata(
31
36
  thinkingLevel: string,
32
37
  theme: ModeStatusTheme,
33
38
  thinkingColor: (text: string) => string,
39
+ options?: PromptMetadataOptions,
34
40
  ): string {
35
41
  const label = MODE_LABELS[mode];
36
42
  const modeText = `\x1b[${label.color}m${theme.bold(label.text)}${ANSI_RESET}`;
37
- return `${formatModeRail(mode)} ${modeText}${theme.fg("dim", " · ")}${thinkingColor(thinkingLevel)}`;
43
+ const modelText = options
44
+ ? `${theme.fg("dim", " • ")}${options.modelColor(options.modelName)}`
45
+ : "";
46
+ const thinkingSeparator = options ? " • " : " · ";
47
+ return `${formatModeRail(mode)} ${modeText}${modelText}${theme.fg("dim", thinkingSeparator)}${thinkingColor(thinkingLevel)}`;
48
+ }
49
+
50
+ export function formatTokens(count: number): string {
51
+ if (count < 1000) return count.toString();
52
+ if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
53
+ if (count < 1000000) return `${Math.round(count / 1000)}k`;
54
+ if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
55
+ return `${Math.round(count / 1000000)}M`;
56
+ }
57
+
58
+ export function formatFooterCwd(cwd: string, home: string | undefined): string {
59
+ if (!home) return cwd;
60
+ const resolvedCwd = resolve(cwd);
61
+ const relativeToHome = relative(resolve(home), resolvedCwd);
62
+ const isInsideHome =
63
+ relativeToHome === "" ||
64
+ (relativeToHome !== ".." && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));
65
+ if (!isInsideHome) return cwd;
66
+ return relativeToHome === "" ? "~" : `~${sep}${relativeToHome}`;
38
67
  }
39
68
 
40
69
  export function renderModeComposer(