mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Unify live bottom chat blocks
This commit is contained in:
parent
82f3e61c10
commit
2ac9a141c9
4 changed files with 213 additions and 23 deletions
|
|
@ -10,7 +10,6 @@ export function displayBlocksForItems(
|
|||
turnDiffs?: ReadonlyMap<string, string>,
|
||||
): DisplayBlock[] {
|
||||
const visibleItems = items.filter(shouldShowDisplayItem);
|
||||
const orderedItems = activeTurnId ? moveActiveTaskProgressToEnd(visibleItems, activeTurnId) : visibleItems;
|
||||
const editedFilesByTurn = editedFilesForTurns(visibleItems, workspaceRoot);
|
||||
const autoReviewSummariesByTurn = autoReviewSummariesForTurns(visibleItems);
|
||||
const finalAssistantIdByTurn = finalAssistantItemsByTurn(visibleItems);
|
||||
|
|
@ -18,7 +17,7 @@ export function displayBlocksForItems(
|
|||
const summaryAssistantIdByTurn = new Map([...finalAssistantIdByTurn].filter(([turnId]) => groupedTurnIds.has(turnId)));
|
||||
|
||||
const groupedActivities = new Map<string, DisplayItem[]>();
|
||||
for (const item of orderedItems) {
|
||||
for (const item of visibleItems) {
|
||||
if (!item.turnId || !groupedTurnIds.has(item.turnId) || !isCompletedTurnDetailItem(item, finalAssistantIdByTurn)) continue;
|
||||
const group = groupedActivities.get(item.turnId) ?? [];
|
||||
group.push(item);
|
||||
|
|
@ -26,7 +25,7 @@ export function displayBlocksForItems(
|
|||
}
|
||||
|
||||
const blocks: DisplayBlock[] = [];
|
||||
for (const item of orderedItems) {
|
||||
for (const item of visibleItems) {
|
||||
const turnId = item.turnId;
|
||||
if (turnId && groupedActivities.has(turnId) && isCompletedTurnDetailItem(item, finalAssistantIdByTurn)) {
|
||||
continue;
|
||||
|
|
@ -50,12 +49,6 @@ export function displayBlocksForItems(
|
|||
return blocks;
|
||||
}
|
||||
|
||||
function moveActiveTaskProgressToEnd(items: readonly DisplayItem[], activeTurnId: string): DisplayItem[] {
|
||||
const activeTaskProgress = items.filter((item) => item.kind === "taskProgress" && item.turnId === activeTurnId);
|
||||
if (activeTaskProgress.length === 0) return [...items];
|
||||
return [...items.filter((item) => item.kind !== "taskProgress" || item.turnId !== activeTurnId), ...activeTaskProgress];
|
||||
}
|
||||
|
||||
function shouldShowDisplayItem(item: DisplayItem): boolean {
|
||||
return item.kind !== "reasoning" || executionState(item) !== "completed" || item.text.trim().length > 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,8 @@ export function messageStreamBlocks(context: MessageStreamContext): MessageStrea
|
|||
return blocks;
|
||||
}
|
||||
|
||||
for (const block of displayBlocksForItems(context.displayItems, activeTurn, context.workspaceRoot, context.turnDiffs)) {
|
||||
const streamItems = activeTurn ? withoutActiveTaskProgress(context.displayItems, activeTurn) : context.displayItems;
|
||||
for (const block of displayBlocksForItems(streamItems, activeTurn, context.workspaceRoot, context.turnDiffs)) {
|
||||
if (block.type === "item") {
|
||||
blocks.push({
|
||||
key: `item:${block.item.id}`,
|
||||
|
|
@ -108,13 +109,14 @@ export function messageStreamBlocks(context: MessageStreamContext): MessageStrea
|
|||
}
|
||||
}
|
||||
|
||||
const agentSummary = activeAgentRunSummaryBlock(context);
|
||||
if (agentSummary) {
|
||||
blocks.push({
|
||||
key: `active-agents:${activeTurn ?? "none"}`,
|
||||
node: agentRunSummaryNode(agentSummary),
|
||||
});
|
||||
}
|
||||
blocks.push(...bottomLiveBlocks(context, activeTurn));
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function bottomLiveBlocks(context: MessageStreamContext, activeTurn: string | null): MessageStreamBlock[] {
|
||||
const blocks: MessageStreamBlock[] = [];
|
||||
if (activeTurn) blocks.push(...activeTurnLiveBlocks(context, activeTurn));
|
||||
|
||||
if (context.renderPendingRequests && context.pendingRequestsSignature) {
|
||||
blocks.push({
|
||||
|
|
@ -122,10 +124,45 @@ export function messageStreamBlocks(context: MessageStreamContext): MessageStrea
|
|||
node: context.renderPendingRequests(),
|
||||
});
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function activeTurnLiveBlocks(context: MessageStreamContext, activeTurn: string): MessageStreamBlock[] {
|
||||
const agentSummaryAnchorId = activeAgentRunSummaryAnchorId(context.displayItems, activeTurn);
|
||||
const agentSummary = agentSummaryAnchorId ? activeAgentRunSummaryBlock(context) : null;
|
||||
const blocks = context.displayItems.flatMap((item): MessageStreamBlock[] => {
|
||||
if (item.kind === "taskProgress" && item.turnId === activeTurn) {
|
||||
return [
|
||||
{
|
||||
key: `live-task:${item.id}`,
|
||||
node: workItemNode(item, context),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (item.id === agentSummaryAnchorId) {
|
||||
return agentSummary
|
||||
? [
|
||||
{
|
||||
key: `live-agents:${activeTurn}`,
|
||||
node: agentRunSummaryNode(agentSummary),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function activeAgentRunSummaryAnchorId(items: readonly DisplayItem[], activeTurn: string): string | null {
|
||||
const firstActiveAgent = items.find((item) => item.kind === "agent" && item.turnId === activeTurn);
|
||||
return firstActiveAgent?.id ?? null;
|
||||
}
|
||||
|
||||
function withoutActiveTaskProgress(items: readonly DisplayItem[], activeTurn: string): DisplayItem[] {
|
||||
return items.filter((item) => item.kind !== "taskProgress" || item.turnId !== activeTurn);
|
||||
}
|
||||
|
||||
export function renderMessageStreamBlocks(parent: HTMLElement, blocks: MessageStreamBlock[]): void {
|
||||
renderReactRoot(parent, <MessageStreamBlocks blocks={blocks} />);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -985,7 +985,7 @@ describe("display block grouping keeps work logs subordinate to conversation mes
|
|||
expect(displayBlocksForItems(items, "t1").map((block) => block.type)).toEqual(["item", "item"]);
|
||||
});
|
||||
|
||||
it("moves active task progress to the end of the live turn", () => {
|
||||
it("keeps active task progress chronological in the display model", () => {
|
||||
const items: DisplayItem[] = [
|
||||
{ id: "u1", kind: "message", role: "user", text: "do it", turnId: "t1" },
|
||||
{
|
||||
|
|
@ -1003,7 +1003,7 @@ describe("display block grouping keeps work logs subordinate to conversation mes
|
|||
|
||||
const blocks = displayBlocksForItems(items, "t1");
|
||||
|
||||
expect(blocks.map((block) => (block.type === "item" ? block.item.id : block.id))).toEqual(["u1", "a1", "plan-progress-t1"]);
|
||||
expect(blocks.map((block) => (block.type === "item" ? block.item.id : block.id))).toEqual(["u1", "plan-progress-t1", "a1"]);
|
||||
});
|
||||
|
||||
it("summarizes task progress and agent activity separately from tools", () => {
|
||||
|
|
|
|||
|
|
@ -1776,13 +1776,173 @@ describe("work log renderer decisions", () => {
|
|||
}),
|
||||
);
|
||||
|
||||
const block = expectPresent(parent.querySelector<HTMLElement>('[data-codex-panel-block-key="item:plan-progress-turn"]'));
|
||||
const block = expectPresent(parent.querySelector<HTMLElement>('[data-codex-panel-block-key="live-task:plan-progress-turn"]'));
|
||||
expect(block.querySelector(".codex-panel__task-progress .codex-panel__message-role")?.textContent).toBe("tasks");
|
||||
expect(block.textContent).toContain("[x]Inspect code");
|
||||
expect(block.textContent).toContain("[>]Patch UI");
|
||||
unmountReactRoot(parent);
|
||||
});
|
||||
|
||||
it("renders active task progress with the shared bottom live blocks", () => {
|
||||
const blocks = messageStreamBlocks({
|
||||
activeThreadId: "thread",
|
||||
turnLifecycle: runningTurnLifecycle("turn"),
|
||||
historyCursor: null,
|
||||
loadingHistory: false,
|
||||
displayItems: [
|
||||
{ id: "u1", kind: "message", role: "user", text: "Do it", turnId: "turn", markdown: true },
|
||||
{
|
||||
id: "plan-progress-turn",
|
||||
kind: "taskProgress",
|
||||
role: "tool",
|
||||
text: "[>] Patch UI",
|
||||
turnId: "turn",
|
||||
explanation: null,
|
||||
steps: [{ step: "Patch UI", status: "inProgress" }],
|
||||
status: "inProgress",
|
||||
},
|
||||
{ id: "a1", kind: "message", role: "assistant", text: "Working", turnId: "turn", markdown: true },
|
||||
{
|
||||
id: "agent-1",
|
||||
kind: "agent",
|
||||
role: "tool",
|
||||
text: "Wait for agent",
|
||||
turnId: "turn",
|
||||
tool: "wait",
|
||||
status: "running",
|
||||
senderThreadId: "parent",
|
||||
receiverThreadIds: ["running"],
|
||||
prompt: null,
|
||||
model: null,
|
||||
reasoningEffort: null,
|
||||
agents: [{ threadId: "running", status: "running", message: "Inspecting renderer" }],
|
||||
},
|
||||
],
|
||||
openDetails: new Set(),
|
||||
loadOlderTurns: vi.fn(),
|
||||
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
||||
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
||||
pendingRequestsSignature: "request:1",
|
||||
renderPendingRequests: () => "Request",
|
||||
});
|
||||
|
||||
expect(blocks.map((block) => block.key)).toEqual([
|
||||
"item:u1",
|
||||
"item:a1",
|
||||
"item:agent-1",
|
||||
"live-task:plan-progress-turn",
|
||||
"live-agents:turn",
|
||||
"pending-requests",
|
||||
]);
|
||||
});
|
||||
|
||||
it("orders shared bottom live blocks by insertion order", () => {
|
||||
const blocks = messageStreamBlocks({
|
||||
activeThreadId: "thread",
|
||||
turnLifecycle: runningTurnLifecycle("turn"),
|
||||
historyCursor: null,
|
||||
loadingHistory: false,
|
||||
displayItems: [
|
||||
{ id: "u1", kind: "message", role: "user", text: "Do it", turnId: "turn", markdown: true },
|
||||
{
|
||||
id: "agent-1",
|
||||
kind: "agent",
|
||||
role: "tool",
|
||||
text: "Wait for agent",
|
||||
turnId: "turn",
|
||||
tool: "wait",
|
||||
status: "running",
|
||||
senderThreadId: "parent",
|
||||
receiverThreadIds: ["running"],
|
||||
prompt: null,
|
||||
model: null,
|
||||
reasoningEffort: null,
|
||||
agents: [{ threadId: "running", status: "running", message: null }],
|
||||
},
|
||||
{
|
||||
id: "plan-progress-turn",
|
||||
kind: "taskProgress",
|
||||
role: "tool",
|
||||
text: "[>] Patch UI",
|
||||
turnId: "turn",
|
||||
explanation: null,
|
||||
steps: [{ step: "Patch UI", status: "inProgress" }],
|
||||
status: "inProgress",
|
||||
},
|
||||
],
|
||||
openDetails: new Set(),
|
||||
loadOlderTurns: vi.fn(),
|
||||
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
||||
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
||||
});
|
||||
|
||||
expect(blocks.map((block) => block.key)).toEqual(["item:u1", "item:agent-1", "live-agents:turn", "live-task:plan-progress-turn"]);
|
||||
});
|
||||
|
||||
it("anchors the live agent summary at the first agent activity", () => {
|
||||
const blocks = messageStreamBlocks({
|
||||
activeThreadId: "thread",
|
||||
turnLifecycle: runningTurnLifecycle("turn"),
|
||||
historyCursor: null,
|
||||
loadingHistory: false,
|
||||
displayItems: [
|
||||
{ id: "u1", kind: "message", role: "user", text: "Do it", turnId: "turn", markdown: true },
|
||||
{
|
||||
id: "agent-spawn",
|
||||
kind: "agent",
|
||||
role: "tool",
|
||||
text: "Spawn agent",
|
||||
turnId: "turn",
|
||||
tool: "spawnAgent",
|
||||
status: "completed",
|
||||
senderThreadId: "parent",
|
||||
receiverThreadIds: ["child"],
|
||||
prompt: "Inspect the renderer.",
|
||||
model: null,
|
||||
reasoningEffort: null,
|
||||
agents: [{ threadId: "child", status: "completed", message: null }],
|
||||
},
|
||||
{
|
||||
id: "plan-progress-turn",
|
||||
kind: "taskProgress",
|
||||
role: "tool",
|
||||
text: "[>] Patch UI",
|
||||
turnId: "turn",
|
||||
explanation: null,
|
||||
steps: [{ step: "Patch UI", status: "inProgress" }],
|
||||
status: "inProgress",
|
||||
},
|
||||
{
|
||||
id: "agent-wait",
|
||||
kind: "agent",
|
||||
role: "tool",
|
||||
text: "Wait for agent",
|
||||
turnId: "turn",
|
||||
tool: "wait",
|
||||
status: "running",
|
||||
senderThreadId: "parent",
|
||||
receiverThreadIds: ["child"],
|
||||
prompt: null,
|
||||
model: null,
|
||||
reasoningEffort: null,
|
||||
agents: [{ threadId: "child", status: "running", message: "Inspecting renderer" }],
|
||||
},
|
||||
],
|
||||
openDetails: new Set(),
|
||||
loadOlderTurns: vi.fn(),
|
||||
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
||||
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
||||
});
|
||||
|
||||
expect(blocks.map((block) => block.key)).toEqual([
|
||||
"item:u1",
|
||||
"item:agent-spawn",
|
||||
"item:agent-wait",
|
||||
"live-agents:turn",
|
||||
"live-task:plan-progress-turn",
|
||||
]);
|
||||
});
|
||||
|
||||
it("renders agent activity with target state and prompt details", () => {
|
||||
const block = messageStreamBlocks({
|
||||
activeThreadId: "thread",
|
||||
|
|
@ -1999,7 +2159,7 @@ describe("work log renderer decisions", () => {
|
|||
}),
|
||||
);
|
||||
|
||||
const summary = expectPresent(parent.querySelector<HTMLElement>('[data-codex-panel-block-key="active-agents:turn"]'));
|
||||
const summary = expectPresent(parent.querySelector<HTMLElement>('[data-codex-panel-block-key="live-agents:turn"]'));
|
||||
expect(summary.querySelector(".codex-panel__agent-summary")?.textContent).toContain("Agents 1 running, 1 done");
|
||||
expect(summary.textContent).toContain("runningrunning: Inspecting renderer");
|
||||
unmountReactRoot(parent);
|
||||
|
|
@ -2061,7 +2221,7 @@ describe("work log renderer decisions", () => {
|
|||
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
||||
});
|
||||
|
||||
expect(blocks.some((block) => block.key.startsWith("active-agents:"))).toBe(false);
|
||||
expect(blocks.some((block) => block.key.startsWith("live-agents:"))).toBe(false);
|
||||
});
|
||||
|
||||
it("marks the live agent summary failed when any subagent fails", () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue