Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c0fff5507 | ||
|
|
10c9db6d87 |
@@ -193,10 +193,13 @@ choices, not daemon ones.
|
|||||||
one collapsed turn activity block. `AgentText` and `AgentThought` rows that
|
one collapsed turn activity block. `AgentText` and `AgentThought` rows that
|
||||||
appear *between* tool rows in the same turn are absorbed into the group
|
appear *between* tool rows in the same turn are absorbed into the group
|
||||||
(intermediate narration); the last `AgentText` with no subsequent tool stays
|
(intermediate narration); the last `AgentText` with no subsequent tool stays
|
||||||
outside as the prominent final answer. The header still reads
|
outside as the prominent final answer. While a turn is still active
|
||||||
"N tool calls" (counting only tool rows). Opening the group reveals tools
|
(responding or thinking), the most recent intermediate `AgentText` or
|
||||||
and intermediate text in chronological order, with the text rendered in a
|
`AgentThought` in the group is kept outside as visible narration — replaced
|
||||||
subdued style.
|
by the next text row when one arrives, and absorbed into the group when the
|
||||||
|
turn ends. The header still reads "N tool calls" (counting only tool rows).
|
||||||
|
Opening the group reveals tools and intermediate text in chronological
|
||||||
|
order, with the text rendered in a subdued style.
|
||||||
- The web client's user, agent, thought, tool-detail, and notice text blocks
|
- The web client's user, agent, thought, tool-detail, and notice text blocks
|
||||||
copy their complete source text when clicked or keyboard-activated.
|
copy their complete source text when clicked or keyboard-activated.
|
||||||
- The web client consumes `available_commands_update` notices as the live
|
- The web client consumes `available_commands_update` notices as the live
|
||||||
|
|||||||
@@ -446,6 +446,40 @@ describe("structured connection: data flow", () => {
|
|||||||
conn.dispose();
|
conn.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps last intermediate text outside group while turn is active", () => {
|
||||||
|
const ws = new FakeSocket();
|
||||||
|
const conn = mount({ sockets: [ws] });
|
||||||
|
ws.open();
|
||||||
|
const respondingState: ReducedState = {
|
||||||
|
turn: { responding: true, thinking: false, active_tool_calls: [] },
|
||||||
|
};
|
||||||
|
ws.emit({
|
||||||
|
type: "transcript_snapshot",
|
||||||
|
snapshot: {
|
||||||
|
cursor: 6,
|
||||||
|
rows: [
|
||||||
|
row("user:1", "user", { content: { type: "text", text: "do it" } }),
|
||||||
|
toolRow("tool:1", "one"),
|
||||||
|
row("agent:1", "agent_text", { content: { type: "text", text: "Now fixing" } }),
|
||||||
|
toolRow("tool:2", "two"),
|
||||||
|
row("agent:2", "agent_text", { content: { type: "text", text: "Found more" } }),
|
||||||
|
toolRow("tool:3", "three"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
ws.emit({ type: "reduced_state", state: respondingState });
|
||||||
|
// While the turn is active, the last intermediate text (agent:2) stays
|
||||||
|
// outside the collapsed group so it's visible as current narration.
|
||||||
|
expect(conn.el.querySelector('[data-row-id="agent:2"]')?.closest(".st-tool-group")).toBeNull();
|
||||||
|
// Earlier intermediate text (agent:1) is still inside the group.
|
||||||
|
expect(conn.el.querySelector('[data-row-id="agent:1"]')?.closest(".st-tool-group")).not.toBeNull();
|
||||||
|
// When the turn ends, all intermediate text is absorbed.
|
||||||
|
ws.emit({ type: "reduced_state", state: idleState });
|
||||||
|
expect(conn.el.querySelector('[data-row-id="agent:2"]')?.closest(".st-tool-group")).not.toBeNull();
|
||||||
|
expect(conn.el.querySelector('[data-row-id="agent:1"]')?.closest(".st-tool-group")).not.toBeNull();
|
||||||
|
conn.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps an expanded running tool open without emitting a click when patched", () => {
|
it("keeps an expanded running tool open without emitting a click when patched", () => {
|
||||||
const ws = new FakeSocket();
|
const ws = new FakeSocket();
|
||||||
const conn = mount({ sockets: [ws] });
|
const conn = mount({ sockets: [ws] });
|
||||||
|
|||||||
+24
-3
@@ -453,6 +453,25 @@ export function createStructuredConnection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// pending at end of a turn = final text/approvals, stays outside (not flushed).
|
// pending at end of a turn = final text/approvals, stays outside (not flushed).
|
||||||
|
|
||||||
|
// While the turn is still active, keep the last intermediate text/thought
|
||||||
|
// row outside the collapsed group so it's visible as the agent's current
|
||||||
|
// narration. It gets absorbed on the next rebuild once newer content
|
||||||
|
// arrives or the turn ends.
|
||||||
|
const turnActive = state.turn.responding || state.turn.thinking;
|
||||||
|
if (turnActive) {
|
||||||
|
const lastGroup = groups.get(currentGid);
|
||||||
|
if (lastGroup) {
|
||||||
|
for (let i = lastGroup.rows.length - 1; i >= 0; i--) {
|
||||||
|
const kind = lastGroup.rows[i].kind;
|
||||||
|
if (kind === "agent_text" || kind === "agent_thought") {
|
||||||
|
lastGroup.rows.splice(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -735,15 +754,17 @@ export function createStructuredConnection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyState(next: ReducedState, connectionGeneration: number): void {
|
function applyState(next: ReducedState, connectionGeneration: number): void {
|
||||||
|
const wasTurnActive = state.turn.responding || state.turn.thinking;
|
||||||
state = next;
|
state = next;
|
||||||
stateSocketGeneration = connectionGeneration;
|
stateSocketGeneration = connectionGeneration;
|
||||||
reconcileAwaitingBegin();
|
reconcileAwaitingBegin();
|
||||||
|
const isTurnActive = state.turn.responding || state.turn.thinking;
|
||||||
const newPendingId = state.pending_approval ? lastRowIdOfKind("approval") : null;
|
const newPendingId = state.pending_approval ? lastRowIdOfKind("approval") : null;
|
||||||
if (newPendingId !== pendingApprovalRowId) {
|
const needsRebuild = newPendingId !== pendingApprovalRowId || wasTurnActive !== isTurnActive;
|
||||||
|
if (needsRebuild) {
|
||||||
updateTranscriptLayout(() => {
|
updateTranscriptLayout(() => {
|
||||||
const prev = pendingApprovalRowId;
|
|
||||||
pendingApprovalRowId = newPendingId;
|
pendingApprovalRowId = newPendingId;
|
||||||
if (prev !== null || newPendingId !== null) rebuildTranscript();
|
rebuildTranscript();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
renderConfig();
|
renderConfig();
|
||||||
|
|||||||
Reference in New Issue
Block a user