Reconcile Pi prerequisites with the integrated runtime while holding admission

This commit is contained in:
Dotta committed 2026-10-08 15:01:33 -05:00
1 parent 20ce062701
commit f620e7157b
313 files changed
+13086 -2187

No files matched your search

@@ -231,4 +231,22 @@ describe("TaskChatProtocolActivityRow", () => {
expect(row?.querySelector("details")).toBeNull();
expect(row?.querySelector('[aria-expanded]')).toBeNull();
});
it.each([
["info", "informational", "Provider update", "lucide-info"],
["warning", "informational", "Warning", "lucide-triangle-alert"],
["error", "failed", "Error", "lucide-triangle-alert"],
["error", "informational", "Error", "lucide-triangle-alert"],
["info", "failed", "Error", "lucide-triangle-alert"],
] as const)("renders %s/%s notice severity without hiding its summary", (severity, status, label, icon) => {
const summary = "Pi estimates this turn at $0.000617. Billing cost is unverified.";
render({
id: "notice", kind: "protocol", surface: "provider_activity", family: "provider_notice",
eventType: "provider.notice.recorded", status, title: "Provider notice", summary,
details: [{ label: "Severity", value: severity }], steps: [], links: [], children: [],
});
expect(container.textContent).toContain(label);
expect(container.querySelector("p")?.textContent).toBe(summary);
expect(container.querySelector('[data-testid="task-chat-protocol-activity-icon"]')?.classList.contains(icon)).toBe(true);
});
});
@@ -1,6 +1,5 @@
import { useId, useState, type ReactNode } from "react";
import {
AlertTriangle,
Check,
ChevronRight,
Circle,
@@ -23,6 +22,7 @@ import {
protocolActivityIsRunning,
protocolActivityLabel,
protocolActivityPresentation,
providerNoticeSeverity,
} from "./task-chat-activity-presentation";
const COMPACT_RESEARCH_RESULT_LIMIT = 5;
@@ -281,14 +281,16 @@ export function TaskChatProtocolActivityRow({ item }: { item: TaskChatProtocolIt
const presentation = protocolActivityPresentation(item);
if (!presentation) return null;
if (item.surface === "provider_activity" && item.family === "provider_notice") {
const NoticeIcon = presentation.icon;
const severity = providerNoticeSeverity(item);
const summary = item.summary
?? item.details.find((entry) => entry.label === "Summary")?.value
?? "The provider reported a notice without a message.";
return (
<div className="flex min-w-0 flex-col gap-1.5 py-1 text-xs" data-testid="task-chat-protocol-activity-row" data-activity-family="provider_notice">
<div className="flex items-center gap-2 text-muted-foreground">
<AlertTriangle className="h-3.5 w-3.5 shrink-0" aria-hidden data-testid="task-chat-protocol-activity-icon" />
<span className="font-medium">{item.status === "failed" ? "Error" : item.details.find(detail => detail.label === "Severity")?.value === "info" ? "Provider update" : "Warning"}</span>
<NoticeIcon className="h-3.5 w-3.5 shrink-0" aria-hidden data-testid="task-chat-protocol-activity-icon" />
<span className="font-medium">{severity === "error" ? "Error" : severity === "info" ? "Provider update" : "Warning"}</span>
</div>
<p className="min-w-0 whitespace-pre-wrap break-words text-foreground">{summary}</p>
{item.details.some(detail => detail.label !== "Summary") ? (
@@ -1,3 +1,4 @@
import { AlertTriangle, Info } from "lucide-react";
import { describe, expect, it } from "vitest";
import { completedActivitySummary } from "./completed-activity-summary";
import type {
@@ -141,4 +142,48 @@ describe("completedActivitySummary", () => {
completedActivitySummary([tool("mcp__github__get_pull_request")]).label,
).toBe("Used connected tools");
});
it("shows the preserved pricing estimate as informational, not a billing receipt", () => {
const summary = "Pi estimates this turn at $0.000617 from its model prices. Billing cost is unverified.";
expect(completedActivitySummary([{
...provider("provider_notice", "informational"), summary,
details: [{ label: "Severity", value: "info" }],
}])).toEqual({ label: summary, fullLabel: summary, icon: Info });
});
it.each(["warning", "error", undefined])("retains warning/error icons and safe notice text (%s)", (severity) => {
const summary = "Provider request needs attention.";
const result = completedActivitySummary([{
...provider("provider_notice", severity === "error" ? "failed" : "informational"),
details: [
{ label: "Summary", value: summary },
...(severity ? [{ label: "Severity", value: severity }] : []),
],
}]);
expect(result).toEqual({ label: summary, fullLabel: summary, icon: AlertTriangle });
});
it("uses a generic fallback without promoting arbitrary detail fields", () => {
expect(completedActivitySummary([{
...provider("provider_notice", "informational"), summary: " ",
details: [{ label: "Raw input", value: "private-command-or-path" }],
}]).label).toBe("Received a provider update");
});
it("does not hide a failed notice behind informational metadata", () => {
expect(completedActivitySummary([{
...provider("provider_notice", "failed"), summary: "Provider failed.",
details: [{ label: "Severity", value: "info" }],
}]).icon).toBe(AlertTriangle);
});
it("groups distinct notices and keeps a later error visible beside actual work", () => {
const notices = ["info", "info", "warning", "error"].map((severity, index) => ({
...provider("provider_notice", "informational"),
id: `notice-${index}`,
summary: `Distinct provider message ${index}`,
details: [{ label: "Severity", value: severity }],
}));
const result = completedActivitySummary([tool("bash"), ...notices]);
expect(result.label).toBe("Provider error reported, ran commands");
expect(result.fullLabel).toBe(result.label);
expect(result.label).not.toContain("Distinct provider message");
});
});
@@ -5,7 +5,7 @@ import {
type ToolFamily,
type ToolIcon,
} from "./tool-taxonomy";
import { protocolActivityPresentation } from "./task-chat-activity-presentation";
import { protocolActivityPresentation, providerNoticeSeverity } from "./task-chat-activity-presentation";
type Activity = TaskChatActivityPhaseItem["items"][number];
const labels: Record<ToolFamily, string> = {
@@ -42,6 +42,12 @@ export function completedActivitySummary(items: Activity[]) {
completed: boolean;
order: number;
}> = [];
const notices: Array<{
summary: string | undefined;
severity: "info" | "warning" | "error";
icon: ToolIcon;
order: number;
}> = [];
for (const [order, item] of items.entries()) {
if (item.kind === "tool") {
const p = toolActivityPresentation({
@@ -67,6 +73,13 @@ export function completedActivitySummary(items: Activity[]) {
} else {
const p = protocolActivityPresentation(item);
if (!p) continue;
if (item.surface === "provider_activity" && item.family === "provider_notice") {
// Reuse only the normalized notice text already available in the row.
const summary = item.summary?.trim()
|| item.details.find((entry) => entry.label === "Summary")?.value.trim();
notices.push({ summary, severity: providerNoticeSeverity(item), icon: p.icon, order });
continue;
}
const family =
item.surface === "provider_activity" ? item.family : item.surface;
const label =
@@ -84,7 +97,6 @@ export function completedActivitySummary(items: Activity[]) {
safety: "Reviewed safety",
terminal: "Ran commands",
wait: "Waited",
provider_notice: "Received a provider update",
workspace_change: "Worked on files",
workspace_file: "Referenced files",
resource: "Added resources",
@@ -94,6 +106,21 @@ export function completedActivitySummary(items: Activity[]) {
}
}
}
if (notices.length) {
const strongest = notices.find((notice) => notice.severity === "error")
?? notices.find((notice) => notice.severity === "warning")
?? notices[0]!;
const label = notices.length === 1
? strongest.summary || "Received a provider update"
: strongest.severity === "error"
? "Provider error reported"
: strongest.severity === "warning"
? "Provider warning reported"
: "Received provider updates";
// One notice category leaves room for actual work. Keep warnings and errors
// visible when the compact label truncates other activity categories.
add(label, strongest.icon, strongest.severity === "info" ? notices[0]!.order : -1);
}
const completedFamilies = new Set(
tools.filter((tool) => tool.completed).map((tool) => tool.family),
);
@@ -9,6 +9,7 @@ import {
FilePenLine,
FileText,
GitBranch,
Info,
ListChecks,
PackageCheck,
Search,
@@ -40,6 +41,12 @@ export interface TaskChatActivityPresentation {
detail?: string;
}
export function providerNoticeSeverity(item: TaskChatProviderActivityItem): "info" | "warning" | "error" {
const severity = item.details.find((entry) => entry.label === "Severity")?.value;
if (item.status === "failed" || severity === "error") return "error";
return severity === "info" ? "info" : "warning";
}
function providerDetail(item: TaskChatProviderActivityItem, ...labels: string[]): string | undefined {
return item.details.find((entry) => labels.includes(entry.label))?.value;
}
@@ -135,7 +142,7 @@ export function providerActivityPresentation(item: TaskChatProviderActivityItem)
case "wait":
return { icon: Clock3, runningLabel: "Waiting", completedLabel: "Finished waiting", failedLabel: "Wait failed", interruptedLabel: "Wait stopped", detail };
case "provider_notice":
return { icon: AlertTriangle, runningLabel: "Provider notice", completedLabel: "Provider notice", failedLabel: "Provider error", interruptedLabel: "Provider notice", detail };
return { icon: providerNoticeSeverity(item) === "info" ? Info : AlertTriangle, runningLabel: "Provider notice", completedLabel: "Provider notice", failedLabel: "Provider error", interruptedLabel: "Provider notice", detail };
}
}
@@ -24,6 +24,8 @@ import {
splitTranscriptAtAnchors,
toolDisplayName,
transcriptToTaskChatItems,
runtimeRequestSegmentContext,
reconcileSegmentRuntimeRequests,
type SettledTurnMergeMeta,
type ThreadBackboneEntry,
} from "./transcript-adapter";
@@ -32,11 +34,43 @@ import type {
TaskChatPlanDocumentItem,
TaskChatTurnItem,
} from "./task-chat-model";
import { latestPendingRuntimeRequest } from "./task-chat-model";
import { providerActivityPresentation } from "./task-chat-activity-presentation";
import { nativeRunEventsToTranscript } from "../transcript/native-run-events";
const TS = "2026-07-31T12:00:00.000Z";
describe("runtime requests across steering sections", () => {
it("selects the newer request when an older pending permission crosses steering", () => {
const older: TranscriptEntry = { kind: "runtime_request", ts: TS, requestId: "older-permission",
requestKind: "permission_approval", turnId: "provider-turn", requestType: "permission", status: "pending",
prompt: "Earlier write", choices: [{ key: "decline", label: "Deny" }], fields: [] };
const newer: TranscriptEntry = { ...older, ts: "2026-07-31T12:00:02.000Z", requestId: "newer-permission", prompt: "Later write" };
const options = { runId: "run-steering", running: true };
const context = runtimeRequestSegmentContext([older, newer], options);
const tail = reconcileSegmentRuntimeRequests(transcriptToTaskChatItems([newer], options), [newer], context, true);
expect(tail.map(item => item.id)).toEqual(["run-steering:runtime-request:older-permission", "run-steering:runtime-request:newer-permission"]);
expect(latestPendingRuntimeRequest(tail)?.requestId).toBe("newer-permission");
});
it.each(["pending", "resolved", "cancelled"] as const)("keeps one %s permission under whole-run authority", status => {
const request: TranscriptEntry = { kind: "runtime_request", ts: TS, requestId: "permission-1",
requestKind: "permission_approval", turnId: "provider-turn", requestType: "permission", status: "pending",
prompt: "Pi write", choices: [{ key: "decline", label: "Deny" }], fields: [] };
const after: TranscriptEntry[] = [{ kind: "assistant", ts: "2026-07-31T12:00:01.000Z", text: "Continued.", channel: "progress" }];
if (status === "resolved") after.push({ ...request, ts: "2026-07-31T12:00:02.000Z", status, resolvedAction: "decline" });
const entries = [request, ...after];
const options = { runId: "run-steering", running: status !== "cancelled" };
const context = runtimeRequestSegmentContext(entries, options);
const history = reconcileSegmentRuntimeRequests(transcriptToTaskChatItems([request], { ...options, running: false }), [request], context);
const tail = reconcileSegmentRuntimeRequests(transcriptToTaskChatItems(after, options), after, context, options.running);
const cards = [...history, ...tail].filter(item => item.kind === "protocol" && item.surface === "runtime_request");
expect(cards).toEqual([expect.objectContaining({ requestId: "permission-1", turnId: "provider-turn", status })]);
expect(history.filter(item => item.kind === "protocol")).toHaveLength(status === "pending" ? 0 : 1);
expect(tail.filter(item => item.kind === "protocol")).toHaveLength(status === "pending" ? 1 : 0);
});
});
describe("accepted native response-wake answers", () => {
const runId = "native-response-wake";
const summary =
@@ -20,6 +20,7 @@ import type {
TaskChatRunResultItem,
TaskChatMessageItem,
TaskChatPlanDocumentItem,
TaskChatRuntimeRequestItem,
} from "./task-chat-model";
import {
humanizeToolName,
@@ -570,6 +571,52 @@ interface TranscriptAdapterOptions {
running: boolean;
}
/** Request authority follows the whole run, even when steering splits its display. */
export function runtimeRequestSegmentContext(
entries: readonly TranscriptEntry[],
options: TranscriptAdapterOptions,
) {
const requests = entries.filter(entry => entry.kind === "runtime_request");
const origins = new Map<string, TranscriptEntry>();
for (const entry of requests) {
if (!origins.has(entry.requestId)) origins.set(entry.requestId, entry);
}
const context = new Map<string, { item: TaskChatRuntimeRequestItem; origin: TranscriptEntry }>();
for (const item of transcriptToTaskChatItems(requests, options)) {
if (item.kind === "protocol" && item.surface === "runtime_request") {
context.set(item.id, { item, origin: origins.get(item.requestId)! });
}
}
return context;
}
/** Carry live requests to the tail; retain closed cards at their original position. */
export function reconcileSegmentRuntimeRequests(
items: readonly TaskChatItem[],
entries: readonly TranscriptEntry[],
context: ReturnType<typeof runtimeRequestSegmentContext>,
carryPending = false,
): TaskChatItem[] {
const owned = new Set(entries);
const retained = new Set<string>();
const result = items.flatMap<TaskChatItem>(item => {
if (item.kind !== "protocol" || item.surface !== "runtime_request") return [item];
const state = context.get(item.id);
if (!state || (state.item.status === "pending" ? !carryPending : !owned.has(state.origin))) return [];
retained.add(item.id);
return [state.item];
});
const carried: TaskChatItem[] = [];
if (carryPending) {
for (const { item } of context.values()) {
if (item.status === "pending" && !retained.has(item.id)) carried.push(item);
}
}
// A carried request predates this section. Keep newer requests last so the
// composer selects the latest input instead of reviving an older one.
return [...carried, ...result];
}
/**
* Reduce a transcript into ordered items, coalescing consecutive thinking and
* assistant deltas and threading tool_result/diff onto their tool_call. Mirrors