authorgravatar for maxtheyoung@gmail.comMaxwell Young <maxtheyoung@gmail.com> 2026-07-20 23:16:57+12:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-20 13:16:57+02:00
loga135f2c2d32bcded09fca32d4147c06df90ec84d
tree5e77c7ec91857409fc0d5788e520d57c84f3f678
parent946b867666be94482e9e41015a304f55fe1e4754
signature Signed by PGP key B5690EEEBB952194

[codex] show complete approval details (#4111)


4 files changed, 81 insertions(+), 2 deletions(-)

apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.approval.test.ts created+33
...@@ -0,0 +1,33 @@
1import {
2 EventId,
3 ProviderDriverKind,
4 RuntimeRequestId,
5 ThreadId,
6 type ProviderRuntimeEvent,
7} from "@t3tools/contracts";
8import { describe, expect, it } from "vite-plus/test";
9
10import { runtimeEventToActivities } from "./ProviderRuntimeIngestion.ts";
11
12describe("runtimeEventToActivities approval details", () => {
13 it("preserves complete multiline command details", () => {
14 const detail = `bun run release -- ${"long-argument ".repeat(20)}\nsecond line`;
15 const event = {
16 type: "request.opened",
17 eventId: EventId.make("evt-request-opened"),
18 provider: ProviderDriverKind.make("codex"),
19 createdAt: "2026-07-18T00:00:00.000Z",
20 threadId: ThreadId.make("thread-1"),
21 requestId: RuntimeRequestId.make("approval-1"),
22 payload: {
23 requestType: "command_execution_approval",
24 detail,
25 },
26 } satisfies ProviderRuntimeEvent;
27
28 const [activity] = runtimeEventToActivities(event);
29
30 expect(activity?.kind).toBe("approval.requested");
31 expect((activity?.payload as Record<string, unknown> | undefined)?.detail).toBe(detail);
32 });
33});
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+2-2
...@@ -306,7 +306,7 @@ function requestKindFromCanonicalRequestType(...@@ -306,7 +306,7 @@ function requestKindFromCanonicalRequestType(
306 }306 }
307}307}
308308
309function runtimeEventToActivities(309export function runtimeEventToActivities(
310 event: ProviderRuntimeEvent,310 event: ProviderRuntimeEvent,
311 taskTitle?: string,311 taskTitle?: string,
312): ReadonlyArray<OrchestrationThreadActivity> {312): ReadonlyArray<OrchestrationThreadActivity> {
...@@ -340,7 +340,7 @@ function runtimeEventToActivities(...@@ -340,7 +340,7 @@ function runtimeEventToActivities(
340 requestId: toApprovalRequestId(event.requestId),340 requestId: toApprovalRequestId(event.requestId),
341 ...(requestKind ? { requestKind } : {}),341 ...(requestKind ? { requestKind } : {}),
342 requestType: event.payload.requestType,342 requestType: event.payload.requestType,
343 ...(event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {}),343 ...(event.payload.detail ? { detail: event.payload.detail } : {}),
344 },344 },
345 turnId: toTurnId(event.turnId) ?? null,345 turnId: toTurnId(event.turnId) ?? null,
346 ...maybeSequence,346 ...maybeSequence,
apps/web/src/components/chat/ComposerPendingApprovalPanel.test.tsx created+28
...@@ -0,0 +1,28 @@
1import { ApprovalRequestId } from "@t3tools/contracts";
2import { renderToStaticMarkup } from "react-dom/server";
3import { describe, expect, it } from "vite-plus/test";
4
5import { ComposerPendingApprovalPanel } from "./ComposerPendingApprovalPanel";
6
7describe("ComposerPendingApprovalPanel", () => {
8 it("renders complete multiline command details without hover or truncation", () => {
9 const detail = `bun run release -- ${"long-argument ".repeat(20)}\nsecond line`;
10 const markup = renderToStaticMarkup(
11 <ComposerPendingApprovalPanel
12 approval={{
13 requestId: ApprovalRequestId.make("approval-1"),
14 requestKind: "command",
15 createdAt: "2026-07-18T00:00:00.000Z",
16 detail,
17 }}
18 pendingCount={1}
19 />,
20 );
21
22 expect(markup).toContain('data-approval-detail="complete"');
23 expect(markup).toContain('aria-label="Command"');
24 expect(markup).toContain(detail);
25 expect(markup).not.toContain("truncate");
26 expect(markup).not.toContain("line-clamp");
27 });
28});
apps/web/src/components/chat/ComposerPendingApprovalPanel.tsx+18
...@@ -16,6 +16,12 @@ export const ComposerPendingApprovalPanel = memo(function ComposerPendingApprova...@@ -16,6 +16,12 @@ export const ComposerPendingApprovalPanel = memo(function ComposerPendingApprova
16 : approval.requestKind === "file-read"16 : approval.requestKind === "file-read"
17 ? "File-read approval requested"17 ? "File-read approval requested"
18 : "File-change approval requested";18 : "File-change approval requested";
19 const detailLabel =
20 approval.requestKind === "command"
21 ? "Command"
22 : approval.requestKind === "file-read"
23 ? "File to read"
24 : "File change";
1925
20 return (26 return (
21 <div className="px-4 py-3.5 sm:px-5 sm:py-4">27 <div className="px-4 py-3.5 sm:px-5 sm:py-4">
...@@ -26,6 +32,18 @@ export const ComposerPendingApprovalPanel = memo(function ComposerPendingApprova...@@ -26,6 +32,18 @@ export const ComposerPendingApprovalPanel = memo(function ComposerPendingApprova
26 <span className="text-xs text-muted-foreground">1/{pendingCount}</span>32 <span className="text-xs text-muted-foreground">1/{pendingCount}</span>
27 ) : null}33 ) : null}
28 </div>34 </div>
35 {approval.detail ? (
36 <div className="mt-3 rounded-lg border border-border/65 bg-background/70 p-3">
37 <p className="text-xs font-medium text-muted-foreground">{detailLabel}</p>
38 <pre
39 aria-label={detailLabel}
40 className="mt-2 max-h-40 overflow-auto whitespace-pre-wrap break-words font-mono text-xs leading-relaxed text-foreground"
41 data-approval-detail="complete"
42 >
43 {approval.detail}
44 </pre>
45 </div>
46 ) : null}
29 </div>47 </div>
30 );48 );
31});49});