refactor: dedupe extension and ui helpers

This commit is contained in:
Peter Steinberger
2026-03-02 19:48:50 +00:00
parent b1c30f0ba9
commit eb816e0551
7 changed files with 170 additions and 226 deletions

View File

@@ -114,6 +114,17 @@ export default function promptUrlWidgetExtension(pi: ExtensionAPI) {
} }
}; };
const renderPromptMatch = (ctx: ExtensionContext, match: PromptMatch) => {
setWidget(ctx, match);
applySessionName(ctx, match);
void fetchGhMetadata(pi, match.kind, match.url).then((meta) => {
const title = meta?.title?.trim();
const authorText = formatAuthor(meta?.author);
setWidget(ctx, match, title, authorText);
applySessionName(ctx, match, title);
});
};
pi.on("before_agent_start", async (event, ctx) => { pi.on("before_agent_start", async (event, ctx) => {
if (!ctx.hasUI) { if (!ctx.hasUI) {
return; return;
@@ -123,14 +134,7 @@ export default function promptUrlWidgetExtension(pi: ExtensionAPI) {
return; return;
} }
setWidget(ctx, match); renderPromptMatch(ctx, match);
applySessionName(ctx, match);
void fetchGhMetadata(pi, match.kind, match.url).then((meta) => {
const title = meta?.title?.trim();
const authorText = formatAuthor(meta?.author);
setWidget(ctx, match, title, authorText);
applySessionName(ctx, match, title);
});
}); });
pi.on("session_switch", async (_event, ctx) => { pi.on("session_switch", async (_event, ctx) => {
@@ -177,14 +181,7 @@ export default function promptUrlWidgetExtension(pi: ExtensionAPI) {
return; return;
} }
setWidget(ctx, match); renderPromptMatch(ctx, match);
applySessionName(ctx, match);
void fetchGhMetadata(pi, match.kind, match.url).then((meta) => {
const title = meta?.title?.trim();
const authorText = formatAuthor(meta?.author);
setWidget(ctx, match, title, authorText);
applySessionName(ctx, match, title);
});
}; };
pi.on("session_start", async (_event, ctx) => { pi.on("session_start", async (_event, ctx) => {

View File

@@ -76,6 +76,28 @@ function resolveVersionFromPackage(command: string, cwd: string): string | null
} }
} }
function resolveVersionCheckResult(params: {
expectedVersion?: string;
installedVersion: string;
installCommand: string;
}): AcpxVersionCheckResult {
if (params.expectedVersion && params.installedVersion !== params.expectedVersion) {
return {
ok: false,
reason: "version-mismatch",
message: `acpx version mismatch: found ${params.installedVersion}, expected ${params.expectedVersion}`,
expectedVersion: params.expectedVersion,
installCommand: params.installCommand,
installedVersion: params.installedVersion,
};
}
return {
ok: true,
version: params.installedVersion,
expectedVersion: params.expectedVersion,
};
}
export async function checkAcpxVersion(params: { export async function checkAcpxVersion(params: {
command: string; command: string;
cwd?: string; cwd?: string;
@@ -131,21 +153,7 @@ export async function checkAcpxVersion(params: {
if (hasExpectedVersion && isUnsupportedVersionProbe(result.stdout, result.stderr)) { if (hasExpectedVersion && isUnsupportedVersionProbe(result.stdout, result.stderr)) {
const installedVersion = resolveVersionFromPackage(params.command, cwd); const installedVersion = resolveVersionFromPackage(params.command, cwd);
if (installedVersion) { if (installedVersion) {
if (expectedVersion && installedVersion !== expectedVersion) { return resolveVersionCheckResult({ expectedVersion, installedVersion, installCommand });
return {
ok: false,
reason: "version-mismatch",
message: `acpx version mismatch: found ${installedVersion}, expected ${expectedVersion}`,
expectedVersion,
installCommand,
installedVersion,
};
}
return {
ok: true,
version: installedVersion,
expectedVersion,
};
} }
} }
const stderr = result.stderr.trim(); const stderr = result.stderr.trim();
@@ -179,22 +187,7 @@ export async function checkAcpxVersion(params: {
}; };
} }
if (expectedVersion && installedVersion !== expectedVersion) { return resolveVersionCheckResult({ expectedVersion, installedVersion, installCommand });
return {
ok: false,
reason: "version-mismatch",
message: `acpx version mismatch: found ${installedVersion}, expected ${expectedVersion}`,
expectedVersion,
installCommand,
installedVersion,
};
}
return {
ok: true,
version: installedVersion,
expectedVersion,
};
} }
let pendingEnsure: Promise<void> | null = null; let pendingEnsure: Promise<void> | null = null;

View File

@@ -182,6 +182,12 @@ type LoadedState = {
}; };
type LabelTarget = "issue" | "pr"; type LabelTarget = "issue" | "pr";
type LabelItemBatch = {
batchIndex: number;
items: LabelItem[];
totalCount: number;
fetchedCount: number;
};
function parseArgs(argv: string[]): ScriptOptions { function parseArgs(argv: string[]): ScriptOptions {
let limit = Number.POSITIVE_INFINITY; let limit = Number.POSITIVE_INFINITY;
@@ -408,9 +414,22 @@ function fetchPullRequestPage(repo: RepoInfo, after: string | null): PullRequest
return pullRequests; return pullRequests;
} }
function* fetchOpenIssueBatches(limit: number): Generator<IssueBatch> { function mapNodeToLabelItem(node: IssuePage["nodes"][number]): LabelItem {
return {
number: node.number,
title: node.title,
body: node.body ?? "",
labels: node.labels?.nodes ?? [],
};
}
function* fetchOpenLabelItemBatches(params: {
limit: number;
kindPlural: "issues" | "pull requests";
fetchPage: (repo: RepoInfo, after: string | null) => IssuePage | PullRequestPage;
}): Generator<LabelItemBatch> {
const repo = resolveRepo(); const repo = resolveRepo();
const results: Issue[] = []; const results: LabelItem[] = [];
let page = 1; let page = 1;
let after: string | null = null; let after: string | null = null;
let totalCount = 0; let totalCount = 0;
@@ -419,33 +438,28 @@ function* fetchOpenIssueBatches(limit: number): Generator<IssueBatch> {
logStep(`Repository: ${repo.owner}/${repo.name}`); logStep(`Repository: ${repo.owner}/${repo.name}`);
while (fetchedCount < limit) { while (fetchedCount < params.limit) {
const pageData = fetchIssuePage(repo, after); const pageData = params.fetchPage(repo, after);
const nodes = pageData.nodes ?? []; const nodes = pageData.nodes ?? [];
totalCount = pageData.totalCount ?? totalCount; totalCount = pageData.totalCount ?? totalCount;
if (page === 1) { if (page === 1) {
logSuccess(`Found ${totalCount} open issues.`); logSuccess(`Found ${totalCount} open ${params.kindPlural}.`);
} }
logInfo(`Fetched page ${page} (${nodes.length} issues).`); logInfo(`Fetched page ${page} (${nodes.length} ${params.kindPlural}).`);
for (const node of nodes) { for (const node of nodes) {
if (fetchedCount >= limit) { if (fetchedCount >= params.limit) {
break; break;
} }
results.push({ results.push(mapNodeToLabelItem(node));
number: node.number,
title: node.title,
body: node.body ?? "",
labels: node.labels?.nodes ?? [],
});
fetchedCount += 1; fetchedCount += 1;
if (results.length >= WORK_BATCH_SIZE) { if (results.length >= WORK_BATCH_SIZE) {
yield { yield {
batchIndex, batchIndex,
issues: results.splice(0, results.length), items: results.splice(0, results.length),
totalCount, totalCount,
fetchedCount, fetchedCount,
}; };
@@ -464,72 +478,39 @@ function* fetchOpenIssueBatches(limit: number): Generator<IssueBatch> {
if (results.length) { if (results.length) {
yield { yield {
batchIndex, batchIndex,
issues: results, items: results,
totalCount, totalCount,
fetchedCount, fetchedCount,
}; };
} }
} }
function* fetchOpenPullRequestBatches(limit: number): Generator<PullRequestBatch> { function* fetchOpenIssueBatches(limit: number): Generator<IssueBatch> {
const repo = resolveRepo(); for (const batch of fetchOpenLabelItemBatches({
const results: PullRequest[] = []; limit,
let page = 1; kindPlural: "issues",
let after: string | null = null; fetchPage: fetchIssuePage,
let totalCount = 0; })) {
let fetchedCount = 0;
let batchIndex = 1;
logStep(`Repository: ${repo.owner}/${repo.name}`);
while (fetchedCount < limit) {
const pageData = fetchPullRequestPage(repo, after);
const nodes = pageData.nodes ?? [];
totalCount = pageData.totalCount ?? totalCount;
if (page === 1) {
logSuccess(`Found ${totalCount} open pull requests.`);
}
logInfo(`Fetched page ${page} (${nodes.length} pull requests).`);
for (const node of nodes) {
if (fetchedCount >= limit) {
break;
}
results.push({
number: node.number,
title: node.title,
body: node.body ?? "",
labels: node.labels?.nodes ?? [],
});
fetchedCount += 1;
if (results.length >= WORK_BATCH_SIZE) {
yield {
batchIndex,
pullRequests: results.splice(0, results.length),
totalCount,
fetchedCount,
};
batchIndex += 1;
}
}
if (!pageData.pageInfo.hasNextPage) {
break;
}
after = pageData.pageInfo.endCursor ?? null;
page += 1;
}
if (results.length) {
yield { yield {
batchIndex, batchIndex: batch.batchIndex,
pullRequests: results, issues: batch.items,
totalCount, totalCount: batch.totalCount,
fetchedCount, fetchedCount: batch.fetchedCount,
};
}
}
function* fetchOpenPullRequestBatches(limit: number): Generator<PullRequestBatch> {
for (const batch of fetchOpenLabelItemBatches({
limit,
kindPlural: "pull requests",
fetchPage: fetchPullRequestPage,
})) {
yield {
batchIndex: batch.batchIndex,
pullRequests: batch.items,
totalCount: batch.totalCount,
fetchedCount: batch.fetchedCount,
}; };
} }
} }

View File

@@ -26,6 +26,23 @@ function createState(request: RequestFn, overrides: Partial<UsageState> = {}): U
}; };
} }
function expectSpecificTimezoneCalls(request: ReturnType<typeof vi.fn>, startCall: number): void {
expect(request).toHaveBeenNthCalledWith(startCall, "sessions.usage", {
startDate: "2026-02-16",
endDate: "2026-02-16",
mode: "specific",
utcOffset: "UTC+5:30",
limit: 1000,
includeContextWeight: true,
});
expect(request).toHaveBeenNthCalledWith(startCall + 1, "usage.cost", {
startDate: "2026-02-16",
endDate: "2026-02-16",
mode: "specific",
utcOffset: "UTC+5:30",
});
}
describe("usage controller date interpretation params", () => { describe("usage controller date interpretation params", () => {
beforeEach(() => { beforeEach(() => {
__test.resetLegacyUsageDateParamsCache(); __test.resetLegacyUsageDateParamsCache();
@@ -48,20 +65,7 @@ describe("usage controller date interpretation params", () => {
await loadUsage(state); await loadUsage(state);
expect(request).toHaveBeenNthCalledWith(1, "sessions.usage", { expectSpecificTimezoneCalls(request, 1);
startDate: "2026-02-16",
endDate: "2026-02-16",
mode: "specific",
utcOffset: "UTC+5:30",
limit: 1000,
includeContextWeight: true,
});
expect(request).toHaveBeenNthCalledWith(2, "usage.cost", {
startDate: "2026-02-16",
endDate: "2026-02-16",
mode: "specific",
utcOffset: "UTC+5:30",
});
}); });
it("sends utc mode without offset when usage timezone is utc", async () => { it("sends utc mode without offset when usage timezone is utc", async () => {
@@ -124,20 +128,7 @@ describe("usage controller date interpretation params", () => {
await loadUsage(state); await loadUsage(state);
expect(request).toHaveBeenNthCalledWith(1, "sessions.usage", { expectSpecificTimezoneCalls(request, 1);
startDate: "2026-02-16",
endDate: "2026-02-16",
mode: "specific",
utcOffset: "UTC+5:30",
limit: 1000,
includeContextWeight: true,
});
expect(request).toHaveBeenNthCalledWith(2, "usage.cost", {
startDate: "2026-02-16",
endDate: "2026-02-16",
mode: "specific",
utcOffset: "UTC+5:30",
});
expect(request).toHaveBeenNthCalledWith(3, "sessions.usage", { expect(request).toHaveBeenNthCalledWith(3, "sessions.usage", {
startDate: "2026-02-16", startDate: "2026-02-16",
endDate: "2026-02-16", endDate: "2026-02-16",

View File

@@ -3,8 +3,7 @@ import {
defaultTitle, defaultTitle,
formatToolDetailText, formatToolDetailText,
normalizeToolName, normalizeToolName,
resolveActionArg, resolveToolVerbAndDetailForArgs,
resolveToolVerbAndDetail,
type ToolDisplaySpec as ToolDisplaySpecBase, type ToolDisplaySpec as ToolDisplaySpecBase,
} from "../../../src/agents/tool-display-common.js"; } from "../../../src/agents/tool-display-common.js";
import type { IconName } from "./icons.ts"; import type { IconName } from "./icons.ts";
@@ -126,12 +125,10 @@ export function resolveToolDisplay(params: {
const icon = (spec?.icon ?? FALLBACK.icon ?? "puzzle") as IconName; const icon = (spec?.icon ?? FALLBACK.icon ?? "puzzle") as IconName;
const title = spec?.title ?? defaultTitle(name); const title = spec?.title ?? defaultTitle(name);
const label = spec?.label ?? title; const label = spec?.label ?? title;
const action = resolveActionArg(params.args); let { verb, detail } = resolveToolVerbAndDetailForArgs({
let { verb, detail } = resolveToolVerbAndDetail({
toolKey: key, toolKey: key,
args: params.args, args: params.args,
meta: params.meta, meta: params.meta,
action,
spec, spec,
fallbackDetailKeys: FALLBACK.detailKeys, fallbackDetailKeys: FALLBACK.detailKeys,
detailMode: "first", detailMode: "first",

View File

@@ -1,4 +1,5 @@
export type UpdateAvailable = import("../../../src/infra/update-startup.js").UpdateAvailable; export type UpdateAvailable = import("../../../src/infra/update-startup.js").UpdateAvailable;
import type { CronJobBase } from "../../../src/cron/types-shared.js";
import type { ConfigUiHints } from "../../../src/shared/config-ui-hints-types.js"; import type { ConfigUiHints } from "../../../src/shared/config-ui-hints-types.js";
import type { import type {
GatewayAgentRow as SharedGatewayAgentRow, GatewayAgentRow as SharedGatewayAgentRow,
@@ -492,22 +493,14 @@ export type CronJobState = {
lastFailureAlertAtMs?: number; lastFailureAlertAtMs?: number;
}; };
export type CronJob = { export type CronJob = CronJobBase<
id: string; CronSchedule,
agentId?: string; CronSessionTarget,
sessionKey?: string; CronWakeMode,
name: string; CronPayload,
description?: string; CronDelivery,
enabled: boolean; CronFailureAlert | false
deleteAfterRun?: boolean; > & {
createdAtMs: number;
updatedAtMs: number;
schedule: CronSchedule;
sessionTarget: CronSessionTarget;
wakeMode: CronWakeMode;
payload: CronPayload;
delivery?: CronDelivery;
failureAlert?: CronFailureAlert | false;
state?: CronJobState; state?: CronJobState;
}; };

View File

@@ -42,6 +42,52 @@ import {
export type { UsageColumnId, SessionLogEntry, SessionLogRole }; export type { UsageColumnId, SessionLogEntry, SessionLogRole };
function createEmptyUsageTotals(): UsageTotals {
return {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
totalCost: 0,
inputCost: 0,
outputCost: 0,
cacheReadCost: 0,
cacheWriteCost: 0,
missingCostEntries: 0,
};
}
function addUsageTotals(
acc: UsageTotals,
usage: {
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
totalTokens: number;
totalCost: number;
inputCost?: number;
outputCost?: number;
cacheReadCost?: number;
cacheWriteCost?: number;
missingCostEntries?: number;
},
): UsageTotals {
acc.input += usage.input;
acc.output += usage.output;
acc.cacheRead += usage.cacheRead;
acc.cacheWrite += usage.cacheWrite;
acc.totalTokens += usage.totalTokens;
acc.totalCost += usage.totalCost;
acc.inputCost += usage.inputCost ?? 0;
acc.outputCost += usage.outputCost ?? 0;
acc.cacheReadCost += usage.cacheReadCost ?? 0;
acc.cacheWriteCost += usage.cacheWriteCost ?? 0;
acc.missingCostEntries += usage.missingCostEntries ?? 0;
return acc;
}
export function renderUsage(props: UsageProps) { export function renderUsage(props: UsageProps) {
// Show loading skeleton if loading and no data yet // Show loading skeleton if loading and no data yet
if (props.loading && !props.totals) { if (props.loading && !props.totals) {
@@ -206,69 +252,15 @@ export function renderUsage(props: UsageProps) {
// Compute totals from sessions // Compute totals from sessions
const computeSessionTotals = (sessions: UsageSessionEntry[]): UsageTotals => { const computeSessionTotals = (sessions: UsageSessionEntry[]): UsageTotals => {
return sessions.reduce( return sessions.reduce(
(acc, s) => { (acc, s) => (s.usage ? addUsageTotals(acc, s.usage) : acc),
if (s.usage) { createEmptyUsageTotals(),
acc.input += s.usage.input;
acc.output += s.usage.output;
acc.cacheRead += s.usage.cacheRead;
acc.cacheWrite += s.usage.cacheWrite;
acc.totalTokens += s.usage.totalTokens;
acc.totalCost += s.usage.totalCost;
acc.inputCost += s.usage.inputCost ?? 0;
acc.outputCost += s.usage.outputCost ?? 0;
acc.cacheReadCost += s.usage.cacheReadCost ?? 0;
acc.cacheWriteCost += s.usage.cacheWriteCost ?? 0;
acc.missingCostEntries += s.usage.missingCostEntries ?? 0;
}
return acc;
},
{
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
totalCost: 0,
inputCost: 0,
outputCost: 0,
cacheReadCost: 0,
cacheWriteCost: 0,
missingCostEntries: 0,
},
); );
}; };
// Compute totals from daily data for selected days (more accurate than session totals) // Compute totals from daily data for selected days (more accurate than session totals)
const computeDailyTotals = (days: string[]): UsageTotals => { const computeDailyTotals = (days: string[]): UsageTotals => {
const matchingDays = props.costDaily.filter((d) => days.includes(d.date)); const matchingDays = props.costDaily.filter((d) => days.includes(d.date));
return matchingDays.reduce( return matchingDays.reduce((acc, day) => addUsageTotals(acc, day), createEmptyUsageTotals());
(acc, d) => {
acc.input += d.input;
acc.output += d.output;
acc.cacheRead += d.cacheRead;
acc.cacheWrite += d.cacheWrite;
acc.totalTokens += d.totalTokens;
acc.totalCost += d.totalCost;
acc.inputCost += d.inputCost ?? 0;
acc.outputCost += d.outputCost ?? 0;
acc.cacheReadCost += d.cacheReadCost ?? 0;
acc.cacheWriteCost += d.cacheWriteCost ?? 0;
return acc;
},
{
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
totalCost: 0,
inputCost: 0,
outputCost: 0,
cacheReadCost: 0,
cacheWriteCost: 0,
missingCostEntries: 0,
},
);
}; };
// Compute display totals and count based on filters // Compute display totals and count based on filters