tau-all-agent 0.2.0 → 0.3.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/extensions/answer.ts +112 -85
- package/extensions/branch-term/index.ts +9 -5
- package/extensions/btw.ts +67 -36
- package/extensions/insights.ts +67 -56
- package/extensions/loop.ts +14 -52
- package/extensions/memory.ts +65 -33
- package/extensions/notify.ts +5 -3
- package/extensions/review/fix.ts +10 -16
- package/extensions/review/git.ts +7 -1
- package/extensions/review/index.ts +2 -2
- package/extensions/review/models.ts +12 -2
- package/extensions/review/review.ts +3 -5
- package/extensions/review/runtime.ts +9 -8
- package/extensions/review/triage.ts +2 -4
- package/extensions/sandbox/README.md +5 -0
- package/extensions/sandbox/bash.ts +83 -85
- package/extensions/sandbox/index.ts +21 -11
- package/extensions/sandbox/permissions/network.ts +8 -3
- package/extensions/sandbox/permissions/unsandboxed.ts +1 -240
- package/extensions/sandbox/runtime.ts +53 -19
- package/extensions/subagent/README.md +58 -1
- package/extensions/subagent/config.ts +118 -0
- package/extensions/subagent/index.ts +54 -22
- package/extensions/telegram/README.md +2 -0
- package/extensions/telegram/assistant-result.d.mts +4 -1
- package/extensions/telegram/assistant-result.mjs +31 -16
- package/extensions/telegram/daemon.mjs +146 -194
- package/extensions/telegram/index.ts +11 -7
- package/extensions/tool-display-mode.ts +29 -50
- package/extensions/usage/README.md +5 -0
- package/extensions/usage/index.ts +139 -80
- package/extensions/websearch/README.md +8 -1
- package/extensions/websearch/config.ts +2 -1
- package/extensions/websearch/index.ts +18 -9
- package/extensions/websearch/providers/{openai-codex.pi.ts → openai.pi.ts} +51 -16
- package/extensions/websearch/providers/shared.ts +10 -3
- package/extensions/websearch/types.ts +2 -1
- package/extensions/worktree.ts +33 -89
- package/package.json +1 -1
- package/skills/oracle/SKILL.md +14 -8
- package/skills/oracle/scripts/oracle +43 -19
- package/skills/update-changelog/SKILL.md +8 -4
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
Key,
|
|
9
9
|
matchesKey,
|
|
10
10
|
type Component,
|
|
11
|
+
type KeybindingsManager,
|
|
11
12
|
type TUI,
|
|
12
13
|
truncateToWidth,
|
|
13
14
|
visibleWidth,
|
|
15
|
+
wrapTextWithAnsi,
|
|
14
16
|
} from "@earendil-works/pi-tui";
|
|
15
17
|
import { createHash } from "node:crypto";
|
|
16
18
|
import { createReadStream, type Dirent } from "node:fs";
|
|
@@ -1599,15 +1601,11 @@ function formatUsageHeader(
|
|
|
1599
1601
|
};
|
|
1600
1602
|
}
|
|
1601
1603
|
|
|
1602
|
-
function renderUsageHeader(
|
|
1603
|
-
plan: string | undefined,
|
|
1604
|
-
fetchedAt: Date | undefined,
|
|
1605
|
-
width: number,
|
|
1606
|
-
): string {
|
|
1604
|
+
function renderUsageHeader(plan: string | undefined, fetchedAt: Date | undefined): string {
|
|
1607
1605
|
const header = formatUsageHeader(plan, fetchedAt);
|
|
1608
1606
|
let text = bold(header.title);
|
|
1609
1607
|
if (header.fetched) text += ` ${dim(header.fetched)}`;
|
|
1610
|
-
return
|
|
1608
|
+
return text;
|
|
1611
1609
|
}
|
|
1612
1610
|
|
|
1613
1611
|
type ResolvedLiveUsageState =
|
|
@@ -1669,31 +1667,25 @@ function renderLiveUsageLines(
|
|
|
1669
1667
|
|
|
1670
1668
|
const resolved = resolveLiveUsageState(tab, liveState);
|
|
1671
1669
|
if (resolved.kind === "loading") {
|
|
1672
|
-
return [
|
|
1670
|
+
return [`${bold("Usage")} ${dim(resolved.message)}`];
|
|
1673
1671
|
}
|
|
1674
1672
|
|
|
1675
1673
|
const lines: string[] = [
|
|
1676
1674
|
renderUsageHeader(
|
|
1677
1675
|
resolved.kind === "ok" ? resolved.snapshot.plan : undefined,
|
|
1678
1676
|
resolved.kind === "ok" ? resolved.snapshot.fetchedAt : undefined,
|
|
1679
|
-
width,
|
|
1680
1677
|
),
|
|
1681
1678
|
];
|
|
1682
1679
|
if (resolved.kind === "message") {
|
|
1683
1680
|
const message =
|
|
1684
1681
|
resolved.tone === "error" ? ansiFg(ERROR_COLOR, resolved.message) : dim(resolved.message);
|
|
1685
|
-
lines.push(
|
|
1682
|
+
lines.push(message);
|
|
1686
1683
|
return lines;
|
|
1687
1684
|
}
|
|
1688
1685
|
|
|
1689
1686
|
for (const item of resolved.snapshot.items) {
|
|
1690
1687
|
lines.push("");
|
|
1691
|
-
lines.push(
|
|
1692
|
-
truncateToWidth(
|
|
1693
|
-
formatLiveUsageItemLine(item, { dimReset: true, emphasizeStatValue: true }),
|
|
1694
|
-
width,
|
|
1695
|
-
),
|
|
1696
|
-
);
|
|
1688
|
+
lines.push(formatLiveUsageItemLine(item, { dimReset: true, emphasizeStatValue: true }));
|
|
1697
1689
|
if (item.kind === "meter" && tab.liveProvider) {
|
|
1698
1690
|
lines.push(renderUsageMeterBar(tab.liveProvider.id, item, width));
|
|
1699
1691
|
}
|
|
@@ -1752,12 +1744,17 @@ class UsageBreakdownComponent implements Component {
|
|
|
1752
1744
|
private rangeIndex = 1;
|
|
1753
1745
|
private measurement: MeasurementMode = "tokens";
|
|
1754
1746
|
private view: BreakdownView = "model";
|
|
1747
|
+
private renderWidth?: number;
|
|
1755
1748
|
private cachedWidth?: number;
|
|
1756
1749
|
private cachedLines?: string[];
|
|
1750
|
+
private scrollOffset = 0;
|
|
1751
|
+
private viewportHeight = 0;
|
|
1752
|
+
private maxScroll = 0;
|
|
1757
1753
|
|
|
1758
1754
|
constructor(
|
|
1759
1755
|
data: BreakdownData,
|
|
1760
1756
|
tui: TUI,
|
|
1757
|
+
private readonly keybindings: KeybindingsManager,
|
|
1761
1758
|
onDone: () => void,
|
|
1762
1759
|
loadLiveUsage: (
|
|
1763
1760
|
provider: UsageProviderDefinition,
|
|
@@ -1771,55 +1768,6 @@ class UsageBreakdownComponent implements Component {
|
|
|
1771
1768
|
this.ensureLiveUsageLoaded();
|
|
1772
1769
|
}
|
|
1773
1770
|
|
|
1774
|
-
private async close(): Promise<void> {
|
|
1775
|
-
if (this.closing) return;
|
|
1776
|
-
this.closing = true;
|
|
1777
|
-
for (const controller of this.liveUsageControllers.values()) {
|
|
1778
|
-
controller.abort();
|
|
1779
|
-
}
|
|
1780
|
-
await Promise.allSettled(this.liveUsageRequests.values());
|
|
1781
|
-
this.onDone();
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
private ensureLiveUsageLoaded(): void {
|
|
1785
|
-
const tab = this.data.tabs[this.tabIndex];
|
|
1786
|
-
const provider = tab?.liveProvider;
|
|
1787
|
-
if (!provider) return;
|
|
1788
|
-
|
|
1789
|
-
const current = this.data.liveUsage.get(provider.id);
|
|
1790
|
-
if (current && current.status !== "loading") return;
|
|
1791
|
-
if (this.liveUsageRequests.has(provider.id)) return;
|
|
1792
|
-
|
|
1793
|
-
const controller = new AbortController();
|
|
1794
|
-
this.liveUsageControllers.set(provider.id, controller);
|
|
1795
|
-
this.data.liveUsage.set(provider.id, { status: "loading" });
|
|
1796
|
-
this.invalidate();
|
|
1797
|
-
this.tui.requestRender();
|
|
1798
|
-
|
|
1799
|
-
const request = this.loadLiveUsage(provider, controller.signal)
|
|
1800
|
-
.then((state) => {
|
|
1801
|
-
if (controller.signal.aborted) return;
|
|
1802
|
-
this.data.liveUsage.set(provider.id, state);
|
|
1803
|
-
})
|
|
1804
|
-
.catch((error) => {
|
|
1805
|
-
if (controller.signal.aborted) return;
|
|
1806
|
-
this.data.liveUsage.set(provider.id, {
|
|
1807
|
-
status: "error",
|
|
1808
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1809
|
-
});
|
|
1810
|
-
})
|
|
1811
|
-
.finally(() => {
|
|
1812
|
-
this.liveUsageRequests.delete(provider.id);
|
|
1813
|
-
this.liveUsageControllers.delete(provider.id);
|
|
1814
|
-
if (!controller.signal.aborted) {
|
|
1815
|
-
this.invalidate();
|
|
1816
|
-
this.tui.requestRender();
|
|
1817
|
-
}
|
|
1818
|
-
});
|
|
1819
|
-
|
|
1820
|
-
this.liveUsageRequests.set(provider.id, request);
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
1771
|
invalidate(): void {
|
|
1824
1772
|
this.cachedWidth = undefined;
|
|
1825
1773
|
this.cachedLines = undefined;
|
|
@@ -1836,6 +1784,29 @@ class UsageBreakdownComponent implements Component {
|
|
|
1836
1784
|
return;
|
|
1837
1785
|
}
|
|
1838
1786
|
|
|
1787
|
+
const width = Math.min(
|
|
1788
|
+
this.renderWidth ?? this.tui.terminal.columns - 2,
|
|
1789
|
+
this.tui.terminal.columns - 2,
|
|
1790
|
+
);
|
|
1791
|
+
if (width < 24 || this.tui.terminal.rows < 7) return;
|
|
1792
|
+
|
|
1793
|
+
const scrollActions = [
|
|
1794
|
+
["tui.altScreen.pageUp", -this.viewportHeight],
|
|
1795
|
+
["tui.altScreen.pageDown", this.viewportHeight],
|
|
1796
|
+
["tui.altScreen.halfPageUp", -Math.max(1, Math.floor(this.viewportHeight / 2))],
|
|
1797
|
+
["tui.altScreen.halfPageDown", Math.max(1, Math.floor(this.viewportHeight / 2))],
|
|
1798
|
+
["tui.altScreen.lineUp", -1],
|
|
1799
|
+
["tui.altScreen.lineDown", 1],
|
|
1800
|
+
["tui.altScreen.top", -Infinity],
|
|
1801
|
+
["tui.altScreen.bottom", Infinity],
|
|
1802
|
+
] as const;
|
|
1803
|
+
for (const [action, delta] of scrollActions) {
|
|
1804
|
+
if (!this.keybindings.matches(data, action)) continue;
|
|
1805
|
+
this.scrollOffset = Math.max(0, Math.min(this.maxScroll, this.scrollOffset + delta));
|
|
1806
|
+
this.tui.requestRender();
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1839
1810
|
if (
|
|
1840
1811
|
matchesKey(data, Key.tab) ||
|
|
1841
1812
|
matchesKey(data, Key.shift("tab")) ||
|
|
@@ -1845,6 +1816,7 @@ class UsageBreakdownComponent implements Component {
|
|
|
1845
1816
|
const index = Math.max(0, order.indexOf(this.measurement));
|
|
1846
1817
|
const direction = matchesKey(data, Key.shift("tab")) ? -1 : 1;
|
|
1847
1818
|
this.measurement = order[(index + order.length + direction) % order.length] ?? "tokens";
|
|
1819
|
+
this.scrollOffset = 0;
|
|
1848
1820
|
this.invalidate();
|
|
1849
1821
|
this.tui.requestRender();
|
|
1850
1822
|
return;
|
|
@@ -1858,6 +1830,7 @@ class UsageBreakdownComponent implements Component {
|
|
|
1858
1830
|
) {
|
|
1859
1831
|
const direction = matchesKey(data, Key.left) || data.toLowerCase() === "h" ? -1 : 1;
|
|
1860
1832
|
this.rangeIndex = (this.rangeIndex + RANGE_DAYS.length + direction) % RANGE_DAYS.length;
|
|
1833
|
+
this.scrollOffset = 0;
|
|
1861
1834
|
this.invalidate();
|
|
1862
1835
|
this.tui.requestRender();
|
|
1863
1836
|
return;
|
|
@@ -1866,6 +1839,7 @@ class UsageBreakdownComponent implements Component {
|
|
|
1866
1839
|
if (data === "[" || data === "]" || data.toLowerCase() === "p" || data.toLowerCase() === "n") {
|
|
1867
1840
|
const direction = data === "[" || data.toLowerCase() === "p" ? -1 : 1;
|
|
1868
1841
|
this.tabIndex = (this.tabIndex + this.data.tabs.length + direction) % this.data.tabs.length;
|
|
1842
|
+
this.scrollOffset = 0;
|
|
1869
1843
|
this.ensureLiveUsageLoaded();
|
|
1870
1844
|
this.invalidate();
|
|
1871
1845
|
this.tui.requestRender();
|
|
@@ -1882,6 +1856,7 @@ class UsageBreakdownComponent implements Component {
|
|
|
1882
1856
|
const index = views.indexOf(this.view);
|
|
1883
1857
|
const direction = matchesKey(data, Key.up) || data.toLowerCase() === "k" ? -1 : 1;
|
|
1884
1858
|
this.view = views[(index + views.length + direction) % views.length] ?? "model";
|
|
1859
|
+
this.scrollOffset = 0;
|
|
1885
1860
|
this.invalidate();
|
|
1886
1861
|
this.tui.requestRender();
|
|
1887
1862
|
return;
|
|
@@ -1890,12 +1865,93 @@ class UsageBreakdownComponent implements Component {
|
|
|
1890
1865
|
const rangeByKey: Record<string, number> = { "1": 0, "2": 1, "3": 2 };
|
|
1891
1866
|
if (Object.hasOwn(rangeByKey, data)) {
|
|
1892
1867
|
this.rangeIndex = rangeByKey[data]!;
|
|
1868
|
+
this.scrollOffset = 0;
|
|
1893
1869
|
this.invalidate();
|
|
1894
1870
|
this.tui.requestRender();
|
|
1895
1871
|
}
|
|
1896
1872
|
}
|
|
1897
1873
|
|
|
1898
1874
|
render(width: number): string[] {
|
|
1875
|
+
this.renderWidth = width;
|
|
1876
|
+
const height = Math.max(0, this.tui.terminal.rows - 2);
|
|
1877
|
+
// Four chrome rows plus at least one document row. Margins are supplied by the overlay.
|
|
1878
|
+
if (height < 5 || width < 24) {
|
|
1879
|
+
this.viewportHeight = 0;
|
|
1880
|
+
return ["q close · resize", "Usage needs 26×7"]
|
|
1881
|
+
.slice(0, height)
|
|
1882
|
+
.map((line) => truncateToWidth(line, width));
|
|
1883
|
+
}
|
|
1884
|
+
const document = this.renderDocument(width);
|
|
1885
|
+
const body = document.slice(2);
|
|
1886
|
+
this.viewportHeight = height - 4;
|
|
1887
|
+
this.maxScroll = Math.max(0, body.length - this.viewportHeight);
|
|
1888
|
+
this.scrollOffset = Math.min(this.scrollOffset, this.maxScroll);
|
|
1889
|
+
const visible = body.slice(this.scrollOffset, this.scrollOffset + this.viewportHeight);
|
|
1890
|
+
const keys = (action: "pageUp" | "pageDown") =>
|
|
1891
|
+
this.keybindings.getKeys(`tui.altScreen.${action}`).join("/") || "unbound";
|
|
1892
|
+
return [
|
|
1893
|
+
...document.slice(0, 2),
|
|
1894
|
+
...visible,
|
|
1895
|
+
truncateToWidth(dim(`${keys("pageUp")}/${keys("pageDown")} scroll`), width),
|
|
1896
|
+
truncateToWidth(
|
|
1897
|
+
dim(
|
|
1898
|
+
`q close · ${this.scrollOffset + 1}–${this.scrollOffset + visible.length}/${body.length}`,
|
|
1899
|
+
),
|
|
1900
|
+
width,
|
|
1901
|
+
),
|
|
1902
|
+
];
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
private async close(): Promise<void> {
|
|
1906
|
+
if (this.closing) return;
|
|
1907
|
+
this.closing = true;
|
|
1908
|
+
for (const controller of this.liveUsageControllers.values()) {
|
|
1909
|
+
controller.abort();
|
|
1910
|
+
}
|
|
1911
|
+
await Promise.allSettled(this.liveUsageRequests.values());
|
|
1912
|
+
this.onDone();
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
private ensureLiveUsageLoaded(): void {
|
|
1916
|
+
const tab = this.data.tabs[this.tabIndex];
|
|
1917
|
+
const provider = tab?.liveProvider;
|
|
1918
|
+
if (!provider) return;
|
|
1919
|
+
|
|
1920
|
+
const current = this.data.liveUsage.get(provider.id);
|
|
1921
|
+
if (current && current.status !== "loading") return;
|
|
1922
|
+
if (this.liveUsageRequests.has(provider.id)) return;
|
|
1923
|
+
|
|
1924
|
+
const controller = new AbortController();
|
|
1925
|
+
this.liveUsageControllers.set(provider.id, controller);
|
|
1926
|
+
this.data.liveUsage.set(provider.id, { status: "loading" });
|
|
1927
|
+
this.invalidate();
|
|
1928
|
+
this.tui.requestRender();
|
|
1929
|
+
|
|
1930
|
+
const request = this.loadLiveUsage(provider, controller.signal)
|
|
1931
|
+
.then((state) => {
|
|
1932
|
+
if (controller.signal.aborted) return;
|
|
1933
|
+
this.data.liveUsage.set(provider.id, state);
|
|
1934
|
+
})
|
|
1935
|
+
.catch((error) => {
|
|
1936
|
+
if (controller.signal.aborted) return;
|
|
1937
|
+
this.data.liveUsage.set(provider.id, {
|
|
1938
|
+
status: "error",
|
|
1939
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1940
|
+
});
|
|
1941
|
+
})
|
|
1942
|
+
.finally(() => {
|
|
1943
|
+
this.liveUsageRequests.delete(provider.id);
|
|
1944
|
+
this.liveUsageControllers.delete(provider.id);
|
|
1945
|
+
if (!controller.signal.aborted) {
|
|
1946
|
+
this.invalidate();
|
|
1947
|
+
this.tui.requestRender();
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
|
|
1951
|
+
this.liveUsageRequests.set(provider.id, request);
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
private renderDocument(width: number): string[] {
|
|
1899
1955
|
if (this.cachedWidth === width && this.cachedLines) return this.cachedLines;
|
|
1900
1956
|
|
|
1901
1957
|
const tab = this.data.tabs[this.tabIndex]!;
|
|
@@ -1938,26 +1994,25 @@ class UsageBreakdownComponent implements Component {
|
|
|
1938
1994
|
`${viewTab("model", "model")}${viewTab("cwd", "cwd")}${viewTab("dow", "dow")}${viewTab("tod", "tod")}`;
|
|
1939
1995
|
|
|
1940
1996
|
const lines: string[] = [];
|
|
1941
|
-
|
|
1942
|
-
lines.push(
|
|
1943
|
-
|
|
1944
|
-
);
|
|
1945
|
-
|
|
1997
|
+
const compactHeader = `${bold("Usage")} ${providerBadge} [${selectedDays}d] [${this.measurement === "sessions" ? "sess" : this.measurement === "messages" ? "msg" : "tok"}] [${this.view}]`;
|
|
1998
|
+
lines.push(truncateToWidth(visibleWidth(header) <= width ? header : compactHeader, width));
|
|
1999
|
+
const navigation = dim("[/] provider · ←/→ range · ↑/↓ view · tab metric · q to close");
|
|
2000
|
+
lines.push(truncateToWidth(navigation, width));
|
|
2001
|
+
if (visibleWidth(compactHeader) > width) lines.push(compactHeader);
|
|
2002
|
+
if (visibleWidth(navigation) > width) lines.push(navigation);
|
|
2003
|
+
lines.push(`${bold(tab.label)} ${dim(historySemanticsText(tab))}`);
|
|
1946
2004
|
lines.push("");
|
|
1947
2005
|
|
|
1948
2006
|
const liveLines = renderLiveUsageLines(tab, liveState, width);
|
|
1949
2007
|
if (liveLines.length > 0) {
|
|
1950
|
-
|
|
2008
|
+
lines.push(...liveLines);
|
|
1951
2009
|
lines.push("");
|
|
1952
2010
|
}
|
|
1953
2011
|
|
|
1954
2012
|
const graphDescriptor =
|
|
1955
2013
|
this.view === "dow" ? `share of ${metric.kind} by weekday` : `${metric.kind}/day`;
|
|
1956
2014
|
lines.push(
|
|
1957
|
-
|
|
1958
|
-
`${rangeSummary(range, selectedDays, metric.kind)}${dim(` (graph: ${graphDescriptor})`)}`,
|
|
1959
|
-
width,
|
|
1960
|
-
),
|
|
2015
|
+
`${rangeSummary(range, selectedDays, metric.kind)}${dim(` (graph: ${graphDescriptor})`)}`,
|
|
1961
2016
|
);
|
|
1962
2017
|
lines.push("");
|
|
1963
2018
|
|
|
@@ -2078,16 +2133,19 @@ class UsageBreakdownComponent implements Component {
|
|
|
2078
2133
|
: this.view === "cwd"
|
|
2079
2134
|
? "Top directories (30d palette):"
|
|
2080
2135
|
: "Time of day:";
|
|
2081
|
-
lines.push(
|
|
2082
|
-
|
|
2136
|
+
lines.push(dim(legendTitle));
|
|
2137
|
+
lines.push(...legendItems);
|
|
2083
2138
|
}
|
|
2084
2139
|
}
|
|
2085
2140
|
|
|
2086
2141
|
lines.push("");
|
|
2087
|
-
|
|
2142
|
+
lines.push(...tableLines);
|
|
2088
2143
|
|
|
2089
2144
|
this.cachedWidth = width;
|
|
2090
|
-
this.cachedLines =
|
|
2145
|
+
this.cachedLines = [
|
|
2146
|
+
...lines.slice(0, 2),
|
|
2147
|
+
...lines.slice(2).flatMap((line) => wrapTextWithAnsi(line, width)),
|
|
2148
|
+
];
|
|
2091
2149
|
return this.cachedLines;
|
|
2092
2150
|
}
|
|
2093
2151
|
}
|
|
@@ -2197,10 +2255,11 @@ export default function usageBreakdownExtension(pi: ExtensionAPI) {
|
|
|
2197
2255
|
}
|
|
2198
2256
|
|
|
2199
2257
|
await ctx.ui.custom<void>(
|
|
2200
|
-
(tui, _theme,
|
|
2201
|
-
new UsageBreakdownComponent(data, tui, done, (provider, signal) =>
|
|
2258
|
+
(tui, _theme, keybindings, done) =>
|
|
2259
|
+
new UsageBreakdownComponent(data, tui, keybindings, done, (provider, signal) =>
|
|
2202
2260
|
fetchLiveUsageForProvider(provider, liveUsageAvailability, ctx, signal),
|
|
2203
2261
|
),
|
|
2262
|
+
{ overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", margin: 1 } },
|
|
2204
2263
|
);
|
|
2205
2264
|
},
|
|
2206
2265
|
});
|
|
@@ -8,7 +8,14 @@ Optionally create `websearch.json` in your Pi agent directory (normally `~/.pi/a
|
|
|
8
8
|
|
|
9
9
|
```json
|
|
10
10
|
{
|
|
11
|
-
"routes": [
|
|
11
|
+
"routes": [
|
|
12
|
+
"pi:openai-codex",
|
|
13
|
+
"pi:openai",
|
|
14
|
+
"pi:anthropic",
|
|
15
|
+
"pi:gemini",
|
|
16
|
+
"firefox:gemini",
|
|
17
|
+
"chromium:gemini"
|
|
18
|
+
],
|
|
12
19
|
"profiles": {
|
|
13
20
|
"firefox": "default-release",
|
|
14
21
|
"chromium": "Default"
|
|
@@ -8,6 +8,7 @@ const WEBSEARCH_CONFIG_PATH = path.join(getAgentDir(), "websearch.json");
|
|
|
8
8
|
|
|
9
9
|
const DEFAULT_ROUTES: WebsearchRouteId[] = [
|
|
10
10
|
"pi:openai-codex",
|
|
11
|
+
"pi:openai",
|
|
11
12
|
"pi:anthropic",
|
|
12
13
|
"pi:gemini",
|
|
13
14
|
"firefox:gemini",
|
|
@@ -26,7 +27,7 @@ function sanitizeRoutes(value: unknown): WebsearchRouteId[] {
|
|
|
26
27
|
for (const item of value) {
|
|
27
28
|
if (item === "firefox:openai-codex" || item === "chromium:openai-codex") {
|
|
28
29
|
throw new Error(
|
|
29
|
-
`Invalid websearch route: ${item} (removed). Use pi:openai-codex and sign in with Pi /login.`,
|
|
30
|
+
`Invalid websearch route: ${item} (removed). Use pi:openai-codex or pi:openai and sign in with Pi /login.`,
|
|
30
31
|
);
|
|
31
32
|
}
|
|
32
33
|
if (typeof item !== "string" || !valid.has(item as WebsearchRouteId)) {
|
|
@@ -19,7 +19,11 @@ import { limitOutput } from "./output.js";
|
|
|
19
19
|
import { isPiAnthropicModel, searchWithPiAnthropic } from "./providers/anthropic.pi.js";
|
|
20
20
|
import { browserGemini } from "./providers/gemini.browser.js";
|
|
21
21
|
import { isPiGeminiModel, searchWithPiGemini } from "./providers/gemini.pi.js";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
isPiOpenAICodexModel,
|
|
24
|
+
isPiOpenAIModel,
|
|
25
|
+
searchWithPiOpenAI,
|
|
26
|
+
} from "./providers/openai.pi.js";
|
|
23
27
|
import type { PiModelSelection } from "./providers/pi-model.shared.js";
|
|
24
28
|
import { getPiModelCandidates, selectNextPiModel } from "./providers/pi-model.shared.js";
|
|
25
29
|
import { isModelUnavailableError } from "./providers/shared.js";
|
|
@@ -37,8 +41,13 @@ type PiRouteId = `pi:${WebsearchBackendId}`;
|
|
|
37
41
|
const PI_ROUTE_HANDLERS: Record<PiRouteId, PiRouteHandler> = {
|
|
38
42
|
"pi:openai-codex": {
|
|
39
43
|
predicate: isPiOpenAICodexModel,
|
|
40
|
-
search:
|
|
41
|
-
fallbackModels: ["gpt-
|
|
44
|
+
search: searchWithPiOpenAI,
|
|
45
|
+
fallbackModels: ["gpt-6-luna", "gpt-6.1-sol"],
|
|
46
|
+
},
|
|
47
|
+
"pi:openai": {
|
|
48
|
+
predicate: isPiOpenAIModel,
|
|
49
|
+
search: searchWithPiOpenAI,
|
|
50
|
+
fallbackModels: ["gpt-6-luna", "gpt-6.1-sol"],
|
|
42
51
|
},
|
|
43
52
|
"pi:anthropic": {
|
|
44
53
|
predicate: isPiAnthropicModel,
|
|
@@ -246,24 +255,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
246
255
|
query: Type.String({ description: "What to search for" }),
|
|
247
256
|
}),
|
|
248
257
|
renderShell: "self",
|
|
249
|
-
renderCall(args, theme) {
|
|
250
|
-
return new Text(formatWebsearchCall(args.query, theme),
|
|
258
|
+
renderCall(args, theme, { outputPad }) {
|
|
259
|
+
return new Text(formatWebsearchCall(args.query, theme), outputPad, 0);
|
|
251
260
|
},
|
|
252
|
-
renderResult(result, { expanded }, theme) {
|
|
261
|
+
renderResult(result, { expanded }, theme, { outputPad }) {
|
|
253
262
|
const content = result.content.find((item) => item.type === "text");
|
|
254
263
|
const text = content?.type === "text" ? content.text : "";
|
|
255
264
|
|
|
256
265
|
if (!expanded) {
|
|
257
|
-
return new Text(formatCollapsedWebsearchResult(text, theme),
|
|
266
|
+
return new Text(formatCollapsedWebsearchResult(text, theme), outputPad, 0);
|
|
258
267
|
}
|
|
259
268
|
|
|
260
269
|
if (!text.trim()) {
|
|
261
|
-
return new Text(`\n${theme.fg("muted", "(no output)")}`,
|
|
270
|
+
return new Text(`\n${theme.fg("muted", "(no output)")}`, outputPad, 0);
|
|
262
271
|
}
|
|
263
272
|
|
|
264
273
|
const container = new Container();
|
|
265
274
|
container.addChild(new Spacer(1));
|
|
266
|
-
container.addChild(new Markdown(text.trim(),
|
|
275
|
+
container.addChild(new Markdown(text.trim(), outputPad, 0, getMarkdownTheme()));
|
|
267
276
|
return container;
|
|
268
277
|
},
|
|
269
278
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
@@ -6,19 +6,20 @@ import type { PiModelSelection } from "./pi-model.shared.js";
|
|
|
6
6
|
import { buildWebsearchPrompt, WEBSEARCH_SYSTEM_PROMPT } from "./search-prompt.shared.js";
|
|
7
7
|
import { applyResolvedHeaders, getResolvedHeader, readEventStream, withTimeout } from "./shared.js";
|
|
8
8
|
|
|
9
|
-
export async function
|
|
9
|
+
export async function searchWithPiOpenAI(
|
|
10
10
|
selection: PiModelSelection,
|
|
11
11
|
query: string,
|
|
12
12
|
signal?: AbortSignal,
|
|
13
13
|
): Promise<WebsearchResult> {
|
|
14
14
|
const apiKey = resolveApiKey(selection);
|
|
15
15
|
if (!apiKey) {
|
|
16
|
-
throw new Error("OpenAI
|
|
16
|
+
throw new Error("OpenAI auth is not configured.");
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const
|
|
19
|
+
const backend = selection.model.provider === "openai-codex" ? "openai-codex" : "openai";
|
|
20
|
+
const result = await runOpenAISearch({
|
|
20
21
|
apiKey,
|
|
21
|
-
|
|
22
|
+
backend,
|
|
22
23
|
model: selection.model.id,
|
|
23
24
|
query,
|
|
24
25
|
baseUrl: selection.model.baseUrl,
|
|
@@ -27,13 +28,17 @@ export async function searchWithPiOpenAICodex(
|
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
return {
|
|
30
|
-
backend
|
|
31
|
+
backend,
|
|
31
32
|
authSource: "pi",
|
|
32
33
|
answer: result.answer,
|
|
33
34
|
sources: result.sources,
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
export function isPiOpenAIModel(model: Model<Api>): boolean {
|
|
39
|
+
return model.api === "openai-responses" && model.provider === "openai";
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
export function isPiOpenAICodexModel(model: Model<Api>): boolean {
|
|
38
43
|
return model.api === "openai-codex-responses" && model.provider === "openai-codex";
|
|
39
44
|
}
|
|
@@ -48,25 +53,27 @@ function resolveApiKey(selection: PiModelSelection): string | undefined {
|
|
|
48
53
|
return match?.[1];
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
async function
|
|
56
|
+
async function runOpenAISearch(options: {
|
|
52
57
|
apiKey: string;
|
|
58
|
+
backend: "openai" | "openai-codex";
|
|
53
59
|
model: string;
|
|
54
60
|
query: string;
|
|
55
61
|
baseUrl?: string;
|
|
56
|
-
accountId?: string;
|
|
57
62
|
headers?: Record<string, string | null>;
|
|
58
63
|
signal?: AbortSignal;
|
|
59
64
|
}): Promise<{ answer: string; sources: WebsearchSource[] }> {
|
|
60
|
-
const
|
|
65
|
+
const codex = options.backend === "openai-codex";
|
|
66
|
+
const accountId = codex ? decodeJwtAccountId(options.apiKey) : undefined;
|
|
67
|
+
const url = codex ? resolveCodexUrl(options.baseUrl) : resolveResponsesUrl(options.baseUrl);
|
|
68
|
+
const response = await fetch(url, {
|
|
61
69
|
method: "POST",
|
|
62
70
|
headers: applyResolvedHeaders(
|
|
63
71
|
{
|
|
64
72
|
authorization: `Bearer ${options.apiKey}`,
|
|
65
|
-
...(
|
|
73
|
+
...(accountId ? { "chatgpt-account-id": accountId } : {}),
|
|
66
74
|
"content-type": "application/json",
|
|
67
75
|
accept: "text/event-stream",
|
|
68
|
-
"OpenAI-Beta": "responses=experimental",
|
|
69
|
-
originator: "pi-websearch",
|
|
76
|
+
...(codex ? { "OpenAI-Beta": "responses=experimental", originator: "pi-websearch" } : {}),
|
|
70
77
|
},
|
|
71
78
|
options.headers,
|
|
72
79
|
),
|
|
@@ -82,6 +89,7 @@ async function runOpenAICodexSearch(options: {
|
|
|
82
89
|
signal: withTimeout(options.signal, 120_000),
|
|
83
90
|
});
|
|
84
91
|
|
|
92
|
+
const sources: WebsearchSource[] = [];
|
|
85
93
|
let answer = "";
|
|
86
94
|
let fallbackAnswer = "";
|
|
87
95
|
let completed = false;
|
|
@@ -110,6 +118,7 @@ async function runOpenAICodexSearch(options: {
|
|
|
110
118
|
.filter((text): text is string => typeof text === "string")
|
|
111
119
|
.join("\n");
|
|
112
120
|
if (fullText) fallbackAnswer = fullText;
|
|
121
|
+
sources.push(...extractAnnotationSources(content));
|
|
113
122
|
}
|
|
114
123
|
|
|
115
124
|
if (
|
|
@@ -119,7 +128,7 @@ async function runOpenAICodexSearch(options: {
|
|
|
119
128
|
) {
|
|
120
129
|
const result = event.response as Record<string, unknown> | undefined;
|
|
121
130
|
if (event.type === "response.incomplete" || result?.status !== "completed") {
|
|
122
|
-
throw new Error("OpenAI
|
|
131
|
+
throw new Error("OpenAI search did not complete successfully.");
|
|
123
132
|
}
|
|
124
133
|
completed = true;
|
|
125
134
|
}
|
|
@@ -137,7 +146,7 @@ async function runOpenAICodexSearch(options: {
|
|
|
137
146
|
throw new Error((error as Record<string, unknown>).message as string);
|
|
138
147
|
}
|
|
139
148
|
}
|
|
140
|
-
throw new Error(eventMessage ?? "OpenAI
|
|
149
|
+
throw new Error(eventMessage ?? "OpenAI search failed.");
|
|
141
150
|
}
|
|
142
151
|
} catch (error) {
|
|
143
152
|
if (error instanceof SyntaxError) return;
|
|
@@ -146,20 +155,46 @@ async function runOpenAICodexSearch(options: {
|
|
|
146
155
|
});
|
|
147
156
|
|
|
148
157
|
if (!completed) {
|
|
149
|
-
throw new Error("OpenAI
|
|
158
|
+
throw new Error("OpenAI stream ended before search completed.");
|
|
150
159
|
}
|
|
151
160
|
|
|
152
161
|
const finalAnswer = (answer || fallbackAnswer).trim();
|
|
153
162
|
if (!finalAnswer) {
|
|
154
|
-
throw new Error("OpenAI
|
|
163
|
+
throw new Error("OpenAI returned an empty response.");
|
|
155
164
|
}
|
|
156
165
|
|
|
157
166
|
return {
|
|
158
167
|
answer: finalAnswer,
|
|
159
|
-
sources: dedupeSources(extractMarkdownSources(finalAnswer)),
|
|
168
|
+
sources: dedupeSources([...sources, ...extractMarkdownSources(finalAnswer)]),
|
|
160
169
|
};
|
|
161
170
|
}
|
|
162
171
|
|
|
172
|
+
function extractAnnotationSources(content: unknown[]): WebsearchSource[] {
|
|
173
|
+
const sources: WebsearchSource[] = [];
|
|
174
|
+
for (const part of content) {
|
|
175
|
+
if (!part || typeof part !== "object" || !("annotations" in part)) continue;
|
|
176
|
+
if (!Array.isArray(part.annotations)) continue;
|
|
177
|
+
for (const annotation of part.annotations) {
|
|
178
|
+
if (
|
|
179
|
+
annotation?.type !== "url_citation" ||
|
|
180
|
+
typeof annotation.url !== "string" ||
|
|
181
|
+
!/^https?:\/\//i.test(annotation.url)
|
|
182
|
+
)
|
|
183
|
+
continue;
|
|
184
|
+
sources.push({
|
|
185
|
+
url: annotation.url,
|
|
186
|
+
title: typeof annotation.title === "string" ? annotation.title : "",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return sources;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function resolveResponsesUrl(baseUrl?: string): string {
|
|
194
|
+
const normalized = (baseUrl || "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
195
|
+
return normalized.endsWith("/responses") ? normalized : `${normalized}/responses`;
|
|
196
|
+
}
|
|
197
|
+
|
|
163
198
|
function resolveCodexUrl(baseUrl = "https://chatgpt.com/backend-api"): string {
|
|
164
199
|
const normalized = String(baseUrl || "https://chatgpt.com/backend-api").replace(/\/+$/, "");
|
|
165
200
|
if (normalized.endsWith("/codex/responses")) return normalized;
|
|
@@ -7,15 +7,22 @@ export function isModelUnavailableError(error: unknown, model: Model<Api>): bool
|
|
|
7
7
|
if (!isRecord(error.body)) return false;
|
|
8
8
|
|
|
9
9
|
const detail = isRecord(error.body.error) ? error.body.error : undefined;
|
|
10
|
-
|
|
10
|
+
const codex = model.provider === "openai-codex" && model.api === "openai-codex-responses";
|
|
11
|
+
const openai = model.provider === "openai" && model.api === "openai-responses";
|
|
12
|
+
if (codex || openai) {
|
|
11
13
|
return (
|
|
12
|
-
(
|
|
14
|
+
(codex &&
|
|
15
|
+
error.status === 400 &&
|
|
13
16
|
Boolean(model.id) &&
|
|
14
17
|
error.body.detail ===
|
|
15
18
|
`The '${model.id}' model is not supported when using Codex with a ChatGPT account.`) ||
|
|
16
19
|
detail?.code === "model_not_found" ||
|
|
17
20
|
detail?.code === "model_not_supported" ||
|
|
18
|
-
detail?.code === "unsupported_model"
|
|
21
|
+
detail?.code === "unsupported_model" ||
|
|
22
|
+
(openai &&
|
|
23
|
+
error.status === 400 &&
|
|
24
|
+
detail?.code === "subscription_sharing_unsupported_capability" &&
|
|
25
|
+
detail.param === "model")
|
|
19
26
|
);
|
|
20
27
|
}
|
|
21
28
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export type WebsearchBackendId = "openai-codex" | "anthropic" | "gemini";
|
|
1
|
+
export type WebsearchBackendId = "openai-codex" | "openai" | "anthropic" | "gemini";
|
|
2
2
|
export type WebsearchAuthSource = "pi" | "firefox" | "chromium";
|
|
3
3
|
export type WebsearchBrowserFamily = "firefox" | "chromium";
|
|
4
4
|
export type WebsearchRouteId =
|
|
5
5
|
| "pi:openai-codex"
|
|
6
|
+
| "pi:openai"
|
|
6
7
|
| "pi:anthropic"
|
|
7
8
|
| "pi:gemini"
|
|
8
9
|
| "firefox:gemini"
|