Diff
1diff --git a/src/core/pipeline.ts b/src/core/pipeline.ts
2index 8d54065e6a4cc82142bed48298ffe0c2cb055467..eb753e9eb56fe6ab26ca19dfd24505d4a7c5a2d5 100644
3--- a/src/core/pipeline.ts
4+++ b/src/core/pipeline.ts
5@@ -141,13 +141,16 @@ export function createPolicyPipeline(options: PolicyPipelineOptions) {
6 if (requests.length === 0) return { decision: EMPTY_DECISION, source: "deterministic" }
7
8 let worst: PolicyEvaluation | undefined
9+ const approvalRequests: PolicyRequest[] = []
10 for (const request of requests) {
11 const result = await evaluate(request, context, evaluationOptions)
12 if (result.decision.decision === "deny") return result
13- if (result.decision.decision === "ask") worst = result
14- else if (!worst) worst = result
15+ if (result.decision.decision === "ask") {
16+ worst = result
17+ approvalRequests.push(request)
18+ } else if (!worst) worst = result
19 }
20- return worst!
21+ return approvalRequests.length > 0 ? { ...worst!, approvalRequests } : worst!
22 }
23
24 return { precheck, evaluate, evaluateMany }
25diff --git a/src/core/types.ts b/src/core/types.ts
26index 384abbbaf31f22532354889e0576dea0781d6dfc..5d84d31b0ef779a8c584a61fee2e878fccae14c2 100644
27--- a/src/core/types.ts
28+++ b/src/core/types.ts
29@@ -110,4 +110,5 @@ export type PolicyEvaluation = {
30 source: DecisionSource
31 rawResponse?: string
32 error?: string
33+ approvalRequests?: PolicyRequest[]
34 }
35diff --git a/src/pi/index.ts b/src/pi/index.ts
36index 7b60cb0492ac072d1f9de708ff70c891b377af77..eaba2f6d0569172544d56f540898cdd559ffeb5e 100644
37--- a/src/pi/index.ts
38+++ b/src/pi/index.ts
39@@ -32,6 +32,21 @@ function inputSummary(toolName: string, input: ToolInput): string {
40 return JSON.stringify(input).slice(0, 500)
41 }
42
43+function approvalMessage(toolName: string, input: ToolInput, result: PolicyEvaluation): string {
44+ const rawResponse = result.error === "Failed to parse response JSON" && result.rawResponse
45+ ? `\n\nLLM response:\n${result.rawResponse}`
46+ : ""
47+ if (toolName !== "bash" || typeof input.command !== "string") {
48+ return `${toolName}: ${inputSummary(toolName, input)}\n\n${result.decision.reason}${rawResponse}`
49+ }
50+
51+ const commands = result.approvalRequests
52+ ?.map((request) => request.input.command)
53+ .filter((command): command is string => typeof command === "string")
54+ ?? [input.command]
55+ return `Entire Bash input:\n${input.command}\n\nCommands requiring review:\n${commands.map((command) => `- ${command}`).join("\n")}\n\n${result.decision.reason}${rawResponse}`
56+}
57+
58 function bypassesPiPolicy(toolName: string): boolean {
59 return toolName === "mcp" || toolName.startsWith("mcp__")
60 }
61@@ -197,10 +212,7 @@ export default function policyEngine(pi: ExtensionAPI) {
62 let approved: boolean
63 try {
64 const title = "Policy approval required"
65- const rawResponse = result.error === "Failed to parse response JSON" && result.rawResponse
66- ? `\n\nLLM response:\n${result.rawResponse}`
67- : ""
68- const message = `${event.toolName}: ${inputSummary(event.toolName, input)}\n\n${result.decision.reason}${rawResponse}`
69+ const message = approvalMessage(event.toolName, input, result)
70 approved = ctx.mode === "tui"
71 ? await ctx.ui.custom(
72 (tui, theme, keybindings, done) => new PolicyApprovalDialog(tui, theme, keybindings, title, message, done),
73diff --git a/test/pi/extension.test.ts b/test/pi/extension.test.ts
74index d885484fc12636cef1a1939228ce88b7af37d992..2e2c22e7174b3e7d8c54be4b31e381a17d8f9d8c 100644
75--- a/test/pi/extension.test.ts
76+++ b/test/pi/extension.test.ts
77@@ -125,6 +125,23 @@ describe("Pi policy extension", () => {
78 expect(message).toContain("LLM response:\nnot JSON")
79 })
80
81+ test("shows the full Bash input and commands requiring review", async () => {
82+ let message = ""
83+ const extension = loadExtension()
84+ await extension.toolCall(
85+ { toolName: "bash", input: { command: "git status; sudo id; pwd" } },
86+ context(true, async (_title: string, value: string) => {
87+ message = value
88+ return false
89+ }),
90+ )
91+
92+ expect(message).toContain("Entire Bash input:\ngit status; sudo id; pwd")
93+ expect(message).toContain("Commands requiring review:\n- sudo id")
94+ expect(message).not.toContain("- git status")
95+ expect(message).not.toContain("- pwd")
96+ })
97+
98 test("allows base64 from a pipe", async () => {
99 let reviews = 0
100 globalThis.fetch = (async (_input, _init) => {