@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.
- package/CHANGELOG.md +132 -51
- package/examples/sdk/04-skills.ts +1 -1
- package/package.json +6 -6
- package/src/core/agent-session.ts +112 -4
- package/src/core/auth-storage.ts +524 -202
- package/src/core/bash-executor.ts +1 -1
- package/src/core/model-registry.ts +7 -0
- package/src/core/python-executor.ts +29 -8
- package/src/core/python-gateway-coordinator.ts +55 -1
- package/src/core/python-prelude.py +201 -8
- package/src/core/tools/find.ts +18 -5
- package/src/core/tools/lsp/index.ts +13 -2
- package/src/core/tools/python.ts +1 -0
- package/src/core/tools/read.ts +4 -4
- package/src/modes/interactive/controllers/command-controller.ts +349 -0
- package/src/modes/interactive/controllers/input-controller.ts +55 -7
- package/src/modes/interactive/interactive-mode.ts +6 -1
- package/src/modes/interactive/types.ts +2 -1
- package/src/prompts/system/system-prompt.md +81 -79
- package/src/prompts/tools/python.md +0 -1
|
@@ -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 {
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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>;
|