Compare commits
6
Commits
sotto
...
trombone-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9f8f02e11 | ||
|
|
50b862b1fa | ||
|
|
4668cf8ece | ||
|
|
638155de03 | ||
|
|
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] });
|
||||||
|
|||||||
+34
-7
@@ -85,10 +85,10 @@ export function createStructuredConnection(
|
|||||||
<div class="structured-main">
|
<div class="structured-main">
|
||||||
<div class="structured-scroll">
|
<div class="structured-scroll">
|
||||||
<div class="structured-rows"></div>
|
<div class="structured-rows"></div>
|
||||||
</div>
|
<div class="structured-activity" role="status" aria-live="polite" hidden>
|
||||||
<div class="structured-activity" role="status" aria-live="polite" hidden>
|
<span class="structured-activity-pulse" aria-hidden="true"></span>
|
||||||
<span class="structured-activity-pulse" aria-hidden="true"></span>
|
<span class="structured-activity-text">Thinking</span>
|
||||||
<span class="structured-activity-text">Thinking</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form class="structured-composer">
|
<form class="structured-composer">
|
||||||
<button type="button" class="btn btn-ghost btn-icon structured-attach" aria-label="Attach files" title="Attach files (up to 20 MiB each)">
|
<button type="button" class="btn btn-ghost btn-icon structured-attach" aria-label="Attach files" title="Attach files (up to 20 MiB each)">
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -698,6 +717,7 @@ export function createStructuredConnection(
|
|||||||
|
|
||||||
function renderActivity(): void {
|
function renderActivity(): void {
|
||||||
const active = state.turn.responding || state.turn.thinking || awaitingBeginGeneration !== null;
|
const active = state.turn.responding || state.turn.thinking || awaitingBeginGeneration !== null;
|
||||||
|
const wasHidden = activityEl.hidden;
|
||||||
activityEl.hidden = !active;
|
activityEl.hidden = !active;
|
||||||
if (!active) {
|
if (!active) {
|
||||||
optimisticTurnStartedAt = null;
|
optimisticTurnStartedAt = null;
|
||||||
@@ -720,6 +740,11 @@ export function createStructuredConnection(
|
|||||||
}
|
}
|
||||||
activityText.textContent = parts.join(" · ");
|
activityText.textContent = parts.join(" · ");
|
||||||
if (activityTimer === null) activityTimer = window.setInterval(renderActivity, 1_000);
|
if (activityTimer === null) activityTimer = window.setInterval(renderActivity, 1_000);
|
||||||
|
// Activity lives inside the scroll area; nudge scroll when it first appears.
|
||||||
|
if (wasHidden && followingBottom) {
|
||||||
|
beginTranscriptLayout(true);
|
||||||
|
finishTranscriptLayout();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reconcileAwaitingBegin(): void {
|
function reconcileAwaitingBegin(): void {
|
||||||
@@ -735,15 +760,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();
|
||||||
|
|||||||
@@ -315,8 +315,8 @@ describe("buildTranscriptRow: tool diffs", () => {
|
|||||||
(el.querySelector(".st-tool-head") as HTMLButtonElement).click();
|
(el.querySelector(".st-tool-head") as HTMLButtonElement).click();
|
||||||
const diff = el.querySelector(".st-diff") as HTMLElement;
|
const diff = el.querySelector(".st-diff") as HTMLElement;
|
||||||
expect(diff).not.toBeNull();
|
expect(diff).not.toBeNull();
|
||||||
expect(diff.querySelector(".st-ln-add")!.textContent).toBe("c");
|
expect(diff.querySelector(".st-ln-add")!.textContent).toBe("+ c");
|
||||||
expect(diff.querySelector(".st-ln-del")!.textContent).toBe("b");
|
expect(diff.querySelector(".st-ln-del")!.textContent).toBe("- b");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("gives the diff its own horizontally-scrolling container", () => {
|
it("gives the diff its own horizontally-scrolling container", () => {
|
||||||
|
|||||||
@@ -223,15 +223,16 @@ function buildDiffBlock(diff: { path: string; oldText?: string | null; newText:
|
|||||||
wrap.appendChild(label);
|
wrap.appendChild(label);
|
||||||
const scroller = el("div", "st-diff");
|
const scroller = el("div", "st-diff");
|
||||||
const pre = document.createElement("pre");
|
const pre = document.createElement("pre");
|
||||||
for (const line of computeDiffLines(diff.oldText, diff.newText)) {
|
const lines = computeDiffLines(diff.oldText, diff.newText);
|
||||||
|
const prefixes: Record<string, string> = { add: "+ ", del: "- ", ctx: " ", hunk: "", eof: "" };
|
||||||
|
for (const line of lines) {
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
span.className = `st-ln st-ln-${line.kind}`;
|
span.className = `st-ln st-ln-${line.kind}`;
|
||||||
span.textContent = line.text;
|
span.textContent = (prefixes[line.kind] ?? "") + line.text;
|
||||||
pre.appendChild(span);
|
pre.appendChild(span);
|
||||||
pre.appendChild(document.createTextNode("\n"));
|
|
||||||
}
|
}
|
||||||
scroller.appendChild(pre);
|
scroller.appendChild(pre);
|
||||||
makeCopyable(scroller, pre.textContent ?? "", "diff");
|
makeCopyable(scroller, lines.map((l) => (prefixes[l.kind] ?? "") + l.text).join("\n"), "diff");
|
||||||
wrap.appendChild(scroller);
|
wrap.appendChild(scroller);
|
||||||
return wrap;
|
return wrap;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -2318,9 +2318,7 @@ input.leaf-rename:focus {
|
|||||||
gap: var(--sp-2);
|
gap: var(--sp-2);
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
padding: var(--sp-1) var(--sp-3);
|
padding: var(--sp-1) var(--sp-3);
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
background: var(--surface-2);
|
|
||||||
font-size: var(--fs-11);
|
font-size: var(--fs-11);
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
@@ -2967,6 +2965,7 @@ input.leaf-rename:focus {
|
|||||||
display: block;
|
display: block;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
padding: 0 var(--sp-3);
|
padding: 0 var(--sp-3);
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.st-ln-add {
|
.st-ln-add {
|
||||||
|
|||||||
Reference in New Issue
Block a user