diff --git a/desktop/src/features/agents/ui/agentSessionTranscript.test.mjs b/desktop/src/features/agents/ui/agentSessionTranscript.test.mjs
index fb6b5ab084..cc6f0467d6 100644
--- a/desktop/src/features/agents/ui/agentSessionTranscript.test.mjs
+++ b/desktop/src/features/agents/ui/agentSessionTranscript.test.mjs
@@ -467,6 +467,82 @@ test("buildTranscript promotes ACP plan updates to first-class plan items", () =
   assert.match(items[0].text, /Build registry/);
 });
 
+test("buildTranscript parses standard ACP plan entries[] into a checklist", () => {
+  const items = buildTranscript([
+    sessionUpdate(90, {
+      sessionUpdate: "plan",
+      entries: [
+        { status: "completed", content: "Read the ticket", priority: "medium" },
+        { status: "in_progress", content: "Write the fix", priority: "medium" },
+        { status: "pending", content: "Open the PR", priority: "medium" },
+      ],
+    }),
+  ]);
+
+  assert.equal(items.length, 1);
+  assert.equal(items[0].type, "plan");
+  assert.equal(items[0].renderClass, "plan");
+  assert.equal(
+    items[0].text,
+    [
+      "- [x] Read the ticket",
+      "- [ ] Write the fix (in progress)",
+      "- [ ] Open the PR",
+    ].join("\n"),
+  );
+});
+
+test("buildTranscript summarizes plan entries[] updates with correct N/M complete", () => {
+  const items = buildTranscript([
+    sessionUpdate(90, {
+      sessionUpdate: "plan",
+      entries: [
+        { status: "pending", content: "Read the ticket" },
+        { status: "pending", content: "Write the fix" },
+      ],
+    }),
+    sessionUpdate(91, {
+      sessionUpdate: "plan",
+      entries: [
+        { status: "completed", content: "Read the ticket" },
+        { status: "in_progress", content: "Write the fix" },
+      ],
+    }),
+  ]);
+
+  const updateMarker = items.find(
+    (item) => item.type === "plan" && item.isUpdate,
+  );
+  assert.ok(updateMarker, "expected a plan update marker item");
+  assert.equal(updateMarker.title, "Plan updated");
+  assert.equal(updateMarker.text, "1/2 complete");
+});
+
+test("buildTranscript treats an empty plan entries[] as an empty checklist, not a JSON fallback", () => {
+  const items = buildTranscript([
+    sessionUpdate(90, {
+      sessionUpdate: "plan",
+      entries: [],
+    }),
+  ]);
+
+  assert.equal(items.length, 1);
+  assert.equal(items[0].type, "plan");
+  assert.equal(items[0].text, "");
+});
+
+test("buildTranscript falls back to raw JSON when a plan update has neither entries nor content", () => {
+  const items = buildTranscript([
+    sessionUpdate(90, {
+      sessionUpdate: "plan",
+    }),
+  ]);
+
+  assert.equal(items.length, 1);
+  assert.equal(items[0].type, "plan");
+  assert.match(items[0].text, /"sessionUpdate":\s*"plan"/);
+});
+
 test("buildTranscript stores first-class render class descriptors for tool items", () => {
   const [item] = toolItems([
     acpToolUpdate(91, {
diff --git a/desktop/src/features/agents/ui/agentSessionTranscript.ts b/desktop/src/features/agents/ui/agentSessionTranscript.ts
index d17237a2ab..48db6f6f5b 100644
--- a/desktop/src/features/agents/ui/agentSessionTranscript.ts
+++ b/desktop/src/features/agents/ui/agentSessionTranscript.ts
@@ -18,6 +18,7 @@ import {
   describeSessionResolved,
   extractBlockText,
   extractContentText,
+  extractPlanText,
   extractPromptText,
   extractTriggeringEventIds,
   extractToolArgs,
@@ -1018,7 +1019,7 @@ export function processTranscriptEvent(
           d,
           `plan:${ch}:${turnKey}`,
           "Plan",
-          extractContentText(update.content) || JSON.stringify(update, null, 2),
+          extractPlanText(update),
           event.timestamp,
           ctx,
           updateType,
diff --git a/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.ts b/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.ts
index eeb62c13bd..8e1830dfc0 100644
--- a/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.ts
+++ b/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.ts
@@ -295,6 +295,36 @@ export function extractBlockText(value: unknown): string {
   return directText || nestedText || rawOutputText || "";
 }
 
+/**
+ * Build markdown checklist text for a `plan` session update.
+ *
+ * The standard ACP shape (`@agentclientprotocol/codex-acp`) sends
+ * `entries[]` — `{ status, content, priority }` — with no top-level
+ * `content` field. Older/non-standard adapters instead send
+ * `content: { type: "text", text }` directly on the update. `entries`
+ * (even empty) is treated as authoritative when present; `content` is
+ * only consulted when `entries` is absent, and the raw update is
+ * stringified only when neither yields usable text.
+ */
+export function extractPlanText(update: Record<string, unknown>): string {
+  if (Array.isArray(update.entries)) {
+    return update.entries
+      .map((entry) => formatPlanEntry(asRecord(entry)))
+      .filter(Boolean)
+      .join("\n");
+  }
+  const contentText = extractContentText(update.content);
+  return contentText || JSON.stringify(update, null, 2);
+}
+
+function formatPlanEntry(entry: Record<string, unknown>): string {
+  const content = asString(entry.content);
+  if (!content) return "";
+  const checkbox = entry.status === "completed" ? "[x]" : "[ ]";
+  const suffix = entry.status === "in_progress" ? " (in progress)" : "";
+  return `- ${checkbox} ${content}${suffix}`;
+}
+
 export function extractToolArgs(
   update: Record<string, unknown>,
 ): Record<string, unknown> {
