@oh-my-pi/pi-coding-agent 6.9.69 → 7.0.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.
@@ -1,6 +1,7 @@
1
1
  import { mkdir, rm } from "node:fs/promises";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
+ import type { UsageLimit, UsageReport } from "@oh-my-pi/pi-ai";
4
5
  import { Loader, Markdown, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui";
5
6
  import { $ } from "bun";
6
7
  import { nanoid } from "nanoid";
@@ -284,6 +285,33 @@ export class CommandController {
284
285
  this.ctx.ui.requestRender();
285
286
  }
286
287
 
288
+ async handleUsageCommand(reports?: UsageReport[] | null): Promise<void> {
289
+ let usageReports = reports ?? null;
290
+ if (!usageReports) {
291
+ const provider = this.ctx.session as { fetchUsageReports?: () => Promise<UsageReport[] | null> };
292
+ if (!provider.fetchUsageReports) {
293
+ this.ctx.showWarning("Usage reporting is not configured for this session.");
294
+ return;
295
+ }
296
+ try {
297
+ usageReports = await provider.fetchUsageReports();
298
+ } catch (error) {
299
+ this.ctx.showError(`Failed to fetch usage data: ${error instanceof Error ? error.message : String(error)}`);
300
+ return;
301
+ }
302
+ }
303
+
304
+ if (!usageReports || usageReports.length === 0) {
305
+ this.ctx.showWarning("No usage data available.");
306
+ return;
307
+ }
308
+
309
+ const output = renderUsageReports(usageReports, theme, Date.now());
310
+ this.ctx.chatContainer.addChild(new Spacer(1));
311
+ this.ctx.chatContainer.addChild(new Text(output, 1, 0));
312
+ this.ctx.ui.requestRender();
313
+ }
314
+
287
315
  handleChangelogCommand(): void {
288
316
  const changelogPath = getChangelogPath();
289
317
  const allEntries = parseChangelog(changelogPath);
@@ -598,3 +626,324 @@ export class CommandController {
598
626
  await this.ctx.flushCompactionQueue({ willRetry: false });
599
627
  }
600
628
  }
629
+
630
+ const BAR_WIDTH = 24;
631
+ const COLUMN_WIDTH = BAR_WIDTH + 2;
632
+
633
+ function formatProviderName(provider: string): string {
634
+ return provider
635
+ .split(/[-_]/g)
636
+ .map((part) => (part ? part[0].toUpperCase() + part.slice(1) : ""))
637
+ .join(" ");
638
+ }
639
+
640
+ function formatNumber(value: number, maxFractionDigits = 1): string {
641
+ return new Intl.NumberFormat("en-US", { maximumFractionDigits: maxFractionDigits }).format(value);
642
+ }
643
+
644
+ function formatUsedAccounts(value: number): string {
645
+ return `${value.toFixed(2)} used`;
646
+ }
647
+
648
+ function formatDuration(ms: number): string {
649
+ const totalSeconds = Math.max(0, Math.round(ms / 1000));
650
+ const minutes = Math.floor(totalSeconds / 60);
651
+ const seconds = totalSeconds % 60;
652
+ const hours = Math.floor(minutes / 60);
653
+ const mins = minutes % 60;
654
+ const days = Math.floor(hours / 24);
655
+ const hrs = hours % 24;
656
+ if (days > 0) return `${days}d ${hrs}h`;
657
+ if (hours > 0) return `${hours}h ${mins}m`;
658
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
659
+ return `${seconds}s`;
660
+ }
661
+
662
+ function formatDurationShort(ms: number): string {
663
+ const totalSeconds = Math.max(0, Math.round(ms / 1000));
664
+ const minutes = Math.floor(totalSeconds / 60);
665
+ const hours = Math.floor(minutes / 60);
666
+ const mins = minutes % 60;
667
+ const days = Math.floor(hours / 24);
668
+ const hrs = hours % 24;
669
+ if (days > 0) return `${days}d${hrs > 0 ? ` ${hrs}h` : ""}`;
670
+ if (hours > 0) return `${hours}h${mins > 0 ? ` ${mins}m` : ""}`;
671
+ if (minutes > 0) return `${minutes}m`;
672
+ return `${totalSeconds}s`;
673
+ }
674
+
675
+ function resolveFraction(limit: UsageLimit): number | undefined {
676
+ const amount = limit.amount;
677
+ if (amount.usedFraction !== undefined) return amount.usedFraction;
678
+ if (amount.used !== undefined && amount.limit !== undefined && amount.limit > 0) {
679
+ return amount.used / amount.limit;
680
+ }
681
+ if (amount.unit === "percent" && amount.used !== undefined) {
682
+ return amount.used / 100;
683
+ }
684
+ return undefined;
685
+ }
686
+
687
+ function resolveProviderUsageTotal(reports: UsageReport[]): number {
688
+ return reports
689
+ .flatMap((report) => report.limits)
690
+ .map((limit) => resolveFraction(limit) ?? 0)
691
+ .reduce((sum, value) => sum + value, 0);
692
+ }
693
+
694
+ function formatLimitTitle(limit: UsageLimit): string {
695
+ const tier = limit.scope.tier;
696
+ if (tier && !limit.label.toLowerCase().includes(tier.toLowerCase())) {
697
+ return `${limit.label} (${tier})`;
698
+ }
699
+ return limit.label;
700
+ }
701
+
702
+ function formatWindowSuffix(label: string, windowLabel: string, uiTheme: typeof theme): string {
703
+ const normalizedLabel = label.toLowerCase();
704
+ const normalizedWindow = windowLabel.toLowerCase();
705
+ if (normalizedWindow === "quota window") return "";
706
+ if (normalizedLabel.includes(normalizedWindow)) return "";
707
+ return uiTheme.fg("dim", `(${windowLabel})`);
708
+ }
709
+
710
+ function formatAccountLabel(limit: UsageLimit, report: UsageReport, index: number): string {
711
+ const email = (report.metadata?.email as string | undefined) ?? limit.scope.accountId;
712
+ if (email) return email;
713
+ const accountId = (report.metadata?.accountId as string | undefined) ?? limit.scope.accountId;
714
+ if (accountId) return accountId;
715
+ return `account ${index + 1}`;
716
+ }
717
+
718
+ function formatResetShort(limit: UsageLimit, nowMs: number): string | undefined {
719
+ if (limit.window?.resetInMs !== undefined) {
720
+ return formatDurationShort(limit.window.resetInMs);
721
+ }
722
+ if (limit.window?.resetsAt !== undefined) {
723
+ return formatDurationShort(limit.window.resetsAt - nowMs);
724
+ }
725
+ return undefined;
726
+ }
727
+
728
+ function formatAccountHeader(limit: UsageLimit, report: UsageReport, index: number, nowMs: number): string {
729
+ const label = formatAccountLabel(limit, report, index);
730
+ const reset = formatResetShort(limit, nowMs);
731
+ if (!reset) return label;
732
+ return `${label} (${reset})`;
733
+ }
734
+
735
+ function padColumn(text: string, width: number): string {
736
+ const visible = visibleWidth(text);
737
+ if (visible >= width) return text;
738
+ return `${text}${" ".repeat(width - visible)}`;
739
+ }
740
+
741
+ function resolveAggregateStatus(limits: UsageLimit[]): UsageLimit["status"] {
742
+ const hasOk = limits.some((limit) => limit.status === "ok");
743
+ const hasWarning = limits.some((limit) => limit.status === "warning");
744
+ const hasExhausted = limits.some((limit) => limit.status === "exhausted");
745
+ if (!hasOk && !hasWarning && !hasExhausted) return "unknown";
746
+ if (hasOk) {
747
+ return hasWarning || hasExhausted ? "warning" : "ok";
748
+ }
749
+ if (hasWarning) return "warning";
750
+ return "exhausted";
751
+ }
752
+
753
+ function isZeroUsage(limit: UsageLimit): boolean {
754
+ const amount = limit.amount;
755
+ if (amount.usedFraction !== undefined) return amount.usedFraction <= 0;
756
+ if (amount.used !== undefined) return amount.used <= 0;
757
+ if (amount.unit === "percent" && amount.used !== undefined) return amount.used <= 0;
758
+ if (amount.remainingFraction !== undefined) return amount.remainingFraction >= 1;
759
+ return false;
760
+ }
761
+
762
+ function isZeroUsageGroup(limits: UsageLimit[]): boolean {
763
+ return limits.length > 0 && limits.every((limit) => isZeroUsage(limit));
764
+ }
765
+
766
+ function formatAggregateAmount(limits: UsageLimit[]): string {
767
+ const fractions = limits
768
+ .map((limit) => resolveFraction(limit))
769
+ .filter((value): value is number => value !== undefined);
770
+ if (fractions.length === limits.length && fractions.length > 0) {
771
+ const sum = fractions.reduce((total, value) => total + value, 0);
772
+ const usedPct = Math.max(sum * 100, 0);
773
+ const remainingPct = Math.max(0, limits.length * 100 - usedPct);
774
+ const avgRemaining = limits.length > 0 ? remainingPct / limits.length : remainingPct;
775
+ return `${formatUsedAccounts(sum)} (${formatNumber(avgRemaining)}% left)`;
776
+ }
777
+
778
+ const amounts = limits
779
+ .map((limit) => limit.amount)
780
+ .filter((amount) => amount.used !== undefined && amount.limit !== undefined && amount.limit > 0);
781
+ if (amounts.length === limits.length && amounts.length > 0) {
782
+ const totalUsed = amounts.reduce((sum, amount) => sum + (amount.used ?? 0), 0);
783
+ const totalLimit = amounts.reduce((sum, amount) => sum + (amount.limit ?? 0), 0);
784
+ const usedPct = totalLimit > 0 ? (totalUsed / totalLimit) * 100 : 0;
785
+ const remainingPct = Math.max(0, 100 - usedPct);
786
+ const usedAccounts = totalLimit > 0 ? (usedPct / 100) * limits.length : 0;
787
+ return `${formatUsedAccounts(usedAccounts)} (${formatNumber(remainingPct)}% left)`;
788
+ }
789
+
790
+ return `Accounts: ${limits.length}`;
791
+ }
792
+
793
+ function resolveResetRange(limits: UsageLimit[], nowMs: number): string | null {
794
+ const resets = limits
795
+ .map((limit) => limit.window?.resetInMs ?? undefined)
796
+ .filter((value): value is number => value !== undefined && Number.isFinite(value) && value > 0);
797
+ if (resets.length === 0) {
798
+ const absolute = limits
799
+ .map((limit) => limit.window?.resetsAt)
800
+ .filter((value): value is number => value !== undefined && Number.isFinite(value) && value > nowMs);
801
+ if (absolute.length === 0) return null;
802
+ const earliest = Math.min(...absolute);
803
+ return `resets at ${new Date(earliest).toLocaleString()}`;
804
+ }
805
+ const minReset = Math.min(...resets);
806
+ const maxReset = Math.max(...resets);
807
+ if (maxReset - minReset > 60_000) {
808
+ return `resets in ${formatDuration(minReset)}–${formatDuration(maxReset)}`;
809
+ }
810
+ return `resets in ${formatDuration(minReset)}`;
811
+ }
812
+
813
+ function resolveStatusIcon(status: UsageLimit["status"], uiTheme: typeof theme): string {
814
+ if (status === "exhausted") return uiTheme.fg("error", uiTheme.status.error);
815
+ if (status === "warning") return uiTheme.fg("warning", uiTheme.status.warning);
816
+ if (status === "ok") return uiTheme.fg("success", uiTheme.status.success);
817
+ return uiTheme.fg("dim", uiTheme.status.pending);
818
+ }
819
+
820
+ function resolveStatusColor(status: UsageLimit["status"]): "success" | "warning" | "error" | "dim" {
821
+ if (status === "exhausted") return "error";
822
+ if (status === "warning") return "warning";
823
+ if (status === "ok") return "success";
824
+ return "dim";
825
+ }
826
+
827
+ function renderUsageBar(limit: UsageLimit, uiTheme: typeof theme): string {
828
+ const fraction = resolveFraction(limit);
829
+ if (fraction === undefined) {
830
+ return uiTheme.fg("dim", `[${"·".repeat(BAR_WIDTH)}]`);
831
+ }
832
+ const clamped = Math.min(Math.max(fraction, 0), 1);
833
+ const filled = Math.round(clamped * BAR_WIDTH);
834
+ const filledBar = "█".repeat(filled);
835
+ const emptyBar = "░".repeat(Math.max(0, BAR_WIDTH - filled));
836
+ const color = resolveStatusColor(limit.status);
837
+ return `${uiTheme.fg("dim", "[")}${uiTheme.fg(color, filledBar)}${uiTheme.fg("dim", emptyBar)}${uiTheme.fg("dim", "]")}`;
838
+ }
839
+
840
+ function renderUsageReports(reports: UsageReport[], uiTheme: typeof theme, nowMs: number): string {
841
+ const lines: string[] = [];
842
+ const latestFetchedAt = Math.max(...reports.map((report) => report.fetchedAt ?? 0));
843
+ const headerSuffix = latestFetchedAt ? ` (${formatDuration(nowMs - latestFetchedAt)} ago)` : "";
844
+ lines.push(uiTheme.bold(uiTheme.fg("accent", `Usage${headerSuffix}`)));
845
+ const grouped = new Map<string, UsageReport[]>();
846
+ for (const report of reports) {
847
+ const list = grouped.get(report.provider) ?? [];
848
+ list.push(report);
849
+ grouped.set(report.provider, list);
850
+ }
851
+ const providerEntries = Array.from(grouped.entries())
852
+ .map(([provider, providerReports]) => ({
853
+ provider,
854
+ providerReports,
855
+ totalUsage: resolveProviderUsageTotal(providerReports),
856
+ }))
857
+ .sort((a, b) => {
858
+ if (a.totalUsage !== b.totalUsage) return a.totalUsage - b.totalUsage;
859
+ return a.provider.localeCompare(b.provider);
860
+ });
861
+
862
+ for (const { provider, providerReports } of providerEntries) {
863
+ lines.push("");
864
+ const providerName = formatProviderName(provider);
865
+
866
+ const limitGroups = new Map<
867
+ string,
868
+ { label: string; windowLabel: string; limits: UsageLimit[]; reports: UsageReport[] }
869
+ >();
870
+ for (const report of providerReports) {
871
+ for (const limit of report.limits) {
872
+ const windowId = limit.window?.id ?? limit.scope.windowId ?? "default";
873
+ const key = `${formatLimitTitle(limit)}|${windowId}`;
874
+ const windowLabel = limit.window?.label ?? windowId;
875
+ const entry = limitGroups.get(key) ?? {
876
+ label: formatLimitTitle(limit),
877
+ windowLabel,
878
+ limits: [],
879
+ reports: [],
880
+ };
881
+ entry.limits.push(limit);
882
+ entry.reports.push(report);
883
+ limitGroups.set(key, entry);
884
+ }
885
+ }
886
+
887
+ const providerAllZero = isZeroUsageGroup(Array.from(limitGroups.values()).flatMap((group) => group.limits));
888
+ if (providerAllZero) {
889
+ const providerTitle = `${resolveStatusIcon("ok", uiTheme)} ${uiTheme.fg("accent", `${providerName} (0%)`)}`;
890
+ lines.push(uiTheme.bold(providerTitle));
891
+ continue;
892
+ }
893
+
894
+ lines.push(uiTheme.bold(uiTheme.fg("accent", providerName)));
895
+
896
+ for (const group of limitGroups.values()) {
897
+ const entries = group.limits.map((limit, index) => ({
898
+ limit,
899
+ report: group.reports[index],
900
+ fraction: resolveFraction(limit),
901
+ index,
902
+ }));
903
+ entries.sort((a, b) => {
904
+ const aFraction = a.fraction ?? -1;
905
+ const bFraction = b.fraction ?? -1;
906
+ if (aFraction !== bFraction) return bFraction - aFraction;
907
+ return a.index - b.index;
908
+ });
909
+ const sortedLimits = entries.map((entry) => entry.limit);
910
+ const sortedReports = entries.map((entry) => entry.report);
911
+
912
+ const status = resolveAggregateStatus(sortedLimits);
913
+ const statusIcon = resolveStatusIcon(status, uiTheme);
914
+ if (isZeroUsageGroup(sortedLimits)) {
915
+ const resetText = resolveResetRange(sortedLimits, nowMs);
916
+ const resetSuffix = resetText ? ` | ${resetText}` : "";
917
+ const windowSuffix = formatWindowSuffix(group.label, group.windowLabel, uiTheme);
918
+ lines.push(
919
+ `${statusIcon} ${uiTheme.bold(group.label)} ${windowSuffix} ${uiTheme.fg(
920
+ "dim",
921
+ `0%${resetSuffix}`,
922
+ )}`.trim(),
923
+ );
924
+ continue;
925
+ }
926
+
927
+ const windowSuffix = formatWindowSuffix(group.label, group.windowLabel, uiTheme);
928
+ lines.push(`${statusIcon} ${uiTheme.bold(group.label)} ${windowSuffix}`.trim());
929
+ const accountLabels = sortedLimits.map((limit, index) =>
930
+ padColumn(formatAccountHeader(limit, sortedReports[index], index, nowMs), COLUMN_WIDTH),
931
+ );
932
+ lines.push(` ${accountLabels.join(" ")}`.trimEnd());
933
+ const bars = sortedLimits.map((limit) => padColumn(renderUsageBar(limit, uiTheme), COLUMN_WIDTH));
934
+ lines.push(` ${bars.join(" ")} ${formatAggregateAmount(sortedLimits)}`.trimEnd());
935
+ const resetText = sortedLimits.length <= 1 ? resolveResetRange(sortedLimits, nowMs) : null;
936
+ if (resetText) {
937
+ lines.push(` ${uiTheme.fg("dim", resetText)}`.trimEnd());
938
+ }
939
+ const notes = sortedLimits.flatMap((limit) => limit.notes ?? []);
940
+ if (notes.length > 0) {
941
+ lines.push(` ${uiTheme.fg("dim", notes.join(" • "))}`.trimEnd());
942
+ }
943
+ }
944
+
945
+ // No per-provider footer; global header shows last check.
946
+ }
947
+
948
+ return lines.join("\n");
949
+ }
@@ -1,4 +1,6 @@
1
- import { rm } from "node:fs/promises";
1
+ import { spawn } from "node:child_process";
2
+ import type { FileHandle } from "node:fs/promises";
3
+ import { open, rm } from "node:fs/promises";
2
4
  import * as os from "node:os";
3
5
  import * as path from "node:path";
4
6
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
@@ -143,6 +145,16 @@ export class InputController {
143
145
 
144
146
  if (!text) return;
145
147
 
148
+ // Continue shortcuts: "." or "c" sends empty message (agent continues, no visible message)
149
+ if (text === "." || text === "c") {
150
+ if (this.ctx.onInputCallback) {
151
+ this.ctx.editor.setText("");
152
+ this.ctx.pendingImages = [];
153
+ this.ctx.onInputCallback({ text: "" });
154
+ }
155
+ return;
156
+ }
157
+
146
158
  const runner = this.ctx.session.extensionRunner;
147
159
  let inputImages = this.ctx.pendingImages.length > 0 ? [...this.ctx.pendingImages] : undefined;
148
160
 
@@ -199,6 +211,11 @@ export class InputController {
199
211
  this.ctx.editor.setText("");
200
212
  return;
201
213
  }
214
+ if (text === "/usage") {
215
+ await this.ctx.handleUsageCommand();
216
+ this.ctx.editor.setText("");
217
+ return;
218
+ }
202
219
  if (text === "/changelog") {
203
220
  this.ctx.handleChangelogCommand();
204
221
  this.ctx.editor.setText("");
@@ -614,6 +631,25 @@ export class InputController {
614
631
  this.ctx.showStatus(`Thinking blocks: ${this.ctx.hideThinkingBlock ? "hidden" : "visible"}`);
615
632
  }
616
633
 
634
+ private getEditorTerminalPath(): string | null {
635
+ if (process.platform === "win32") {
636
+ return null;
637
+ }
638
+ return "/dev/tty";
639
+ }
640
+
641
+ private async openEditorTerminalHandle(): Promise<FileHandle | null> {
642
+ const terminalPath = this.getEditorTerminalPath();
643
+ if (!terminalPath) {
644
+ return null;
645
+ }
646
+ try {
647
+ return await open(terminalPath, "r+");
648
+ } catch {
649
+ return null;
650
+ }
651
+ }
652
+
617
653
  async openExternalEditor(): Promise<void> {
618
654
  // Determine editor (respect $VISUAL, then $EDITOR)
619
655
  const editorCmd = process.env.VISUAL || process.env.EDITOR;
@@ -625,23 +661,27 @@ export class InputController {
625
661
  const currentText = this.ctx.editor.getText();
626
662
  const tmpFile = path.join(os.tmpdir(), `omp-editor-${nanoid()}.omp.md`);
627
663
 
664
+ let ttyHandle: FileHandle | null = null;
628
665
  try {
629
666
  // Write current content to temp file
630
667
  await Bun.write(tmpFile, currentText);
631
668
 
632
669
  // Stop TUI to release terminal
670
+ ttyHandle = await this.openEditorTerminalHandle();
633
671
  this.ctx.ui.stop();
634
672
 
635
673
  // Split by space to support editor arguments (e.g., "code --wait")
636
674
  const [editor, ...editorArgs] = editorCmd.split(" ");
637
675
 
638
- // Spawn editor synchronously with inherited stdio for interactive editing
639
- const child = Bun.spawn([editor, ...editorArgs, tmpFile], {
640
- stdin: "inherit",
641
- stdout: "inherit",
642
- stderr: "inherit",
676
+ const stdio: [number | "inherit", number | "inherit", number | "inherit"] = ttyHandle
677
+ ? [ttyHandle.fd, ttyHandle.fd, ttyHandle.fd]
678
+ : ["inherit", "inherit", "inherit"];
679
+
680
+ const child = spawn(editor, [...editorArgs, tmpFile], { stdio });
681
+ const exitCode = await new Promise<number>((resolve, reject) => {
682
+ child.once("exit", (code, signal) => resolve(code ?? (signal ? -1 : 0)));
683
+ child.once("error", (error) => reject(error));
643
684
  });
644
- const exitCode = await child.exited;
645
685
 
646
686
  // On successful exit (exitCode 0), replace editor content
647
687
  if (exitCode === 0) {
@@ -649,6 +689,10 @@ export class InputController {
649
689
  this.ctx.editor.setText(newContent);
650
690
  }
651
691
  // On non-zero exit, keep original text (no action needed)
692
+ } catch (error) {
693
+ this.ctx.showWarning(
694
+ `Failed to open external editor: ${error instanceof Error ? error.message : String(error)}`,
695
+ );
652
696
  } finally {
653
697
  // Clean up temp file
654
698
  try {
@@ -657,6 +701,10 @@ export class InputController {
657
701
  // Ignore cleanup errors
658
702
  }
659
703
 
704
+ if (ttyHandle) {
705
+ await ttyHandle.close();
706
+ }
707
+
660
708
  // Restart TUI
661
709
  this.ctx.ui.start();
662
710
  this.ctx.ui.requestRender();
@@ -5,7 +5,7 @@
5
5
 
6
6
  import * as path from "node:path";
7
7
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
8
- import type { AssistantMessage, ImageContent, Message } from "@oh-my-pi/pi-ai";
8
+ import type { AssistantMessage, ImageContent, Message, UsageReport } from "@oh-my-pi/pi-ai";
9
9
  import type { Component, Loader, SlashCommand } from "@oh-my-pi/pi-tui";
10
10
  import {
11
11
  CombinedAutocompleteProvider,
@@ -187,6 +187,7 @@ export class InteractiveMode implements InteractiveModeContext {
187
187
  { name: "share", description: "Share session as a secret GitHub gist" },
188
188
  { name: "copy", description: "Copy last agent message to clipboard" },
189
189
  { name: "session", description: "Show session info and stats" },
190
+ { name: "usage", description: "Show provider usage and limits" },
190
191
  { name: "extensions", description: "Open Extension Control Center dashboard" },
191
192
  { name: "status", description: "Alias for /extensions" },
192
193
  { name: "changelog", description: "Show changelog entries" },
@@ -613,6 +614,10 @@ export class InteractiveMode implements InteractiveModeContext {
613
614
  this.commandController.handleSessionCommand();
614
615
  }
615
616
 
617
+ handleUsageCommand(reports?: UsageReport[] | null): Promise<void> {
618
+ return this.commandController.handleUsageCommand(reports);
619
+ }
620
+
616
621
  handleChangelogCommand(): void {
617
622
  this.commandController.handleChangelogCommand();
618
623
  }
@@ -1,5 +1,5 @@
1
1
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
- import type { AssistantMessage, ImageContent, Message } from "@oh-my-pi/pi-ai";
2
+ import type { AssistantMessage, ImageContent, Message, UsageReport } from "@oh-my-pi/pi-ai";
3
3
  import type { Component, Container, Loader, Spacer, Text, TUI } from "@oh-my-pi/pi-tui";
4
4
  import type { AgentSession, AgentSessionEvent } from "../../core/agent-session";
5
5
  import type { ExtensionUIContext } from "../../core/extensions/index";
@@ -134,6 +134,7 @@ export interface InteractiveModeContext {
134
134
  handleShareCommand(): Promise<void>;
135
135
  handleCopyCommand(): Promise<void>;
136
136
  handleSessionCommand(): void;
137
+ handleUsageCommand(reports?: UsageReport[] | null): Promise<void>;
137
138
  handleChangelogCommand(): void;
138
139
  handleHotkeysCommand(): void;
139
140
  handleDumpCommand(): Promise<void>;