mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): parse codex ACP plan entries[] into checklist (#1824)
Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
co-authored by
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
parent
020ac7f405
commit
34dcd13d55
@@ -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, {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user