Fix chat input lag

This commit is contained in:
George Pickett
2026-02-18 15:16:22 -08:00
parent 89155f538b
commit e487f3fd16
4 changed files with 100 additions and 43 deletions
@@ -184,6 +184,7 @@ const ToolCallDetails = memo(function ToolCallDetails({
className?: string;
}) {
const { summaryText, body, inlineOnly } = summarizeToolLabel(line);
const [open, setOpen] = useState(false);
const resolvedClassName =
className ??
`w-full ${ASSISTANT_MAX_WIDTH_EXPANDED_CLASS} ${ASSISTANT_GUTTER_CLASS} self-start rounded-[8px] border border-border/70 bg-surface-3 px-2 py-1 text-[10px] text-muted-foreground`;
@@ -195,13 +196,17 @@ const ToolCallDetails = memo(function ToolCallDetails({
);
}
return (
<details
className={resolvedClassName}
>
<summary className="cursor-pointer select-none font-mono text-[10px] font-semibold tracking-[0.11em]">
<details open={open} className={resolvedClassName}>
<summary
className="cursor-pointer select-none font-mono text-[10px] font-semibold tracking-[0.11em]"
onClick={(event) => {
event.preventDefault();
setOpen((current) => !current);
}}
>
{summaryText}
</summary>
{body ? (
{open && body ? (
<div className="agent-markdown agent-tool-markdown mt-1 text-foreground">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{rewriteMediaLinesToMarkdown(body)}
@@ -225,6 +230,7 @@ const ThinkingDetailsRow = memo(function ThinkingDetailsRow({
durationMs?: number;
showTyping?: boolean;
}) {
const [open, setOpen] = useState(false);
const traceEvents = (() => {
if (events && events.length > 0) return events;
const normalizedThinkingText = thinkingText?.trim() ?? "";
@@ -239,8 +245,17 @@ const ThinkingDetailsRow = memo(function ThinkingDetailsRow({
})();
if (traceEvents.length === 0) return null;
return (
<details className="group rounded-[8px] border border-border/70 bg-surface-2 px-2 py-1.5 text-[10px] text-muted-foreground/80">
<summary className="flex cursor-pointer list-none items-center gap-2 opacity-65 [&::-webkit-details-marker]:hidden">
<details
open={open}
className="group rounded-[8px] border border-border/70 bg-surface-2 px-2 py-1.5 text-[10px] text-muted-foreground/80"
>
<summary
className="flex cursor-pointer list-none items-center gap-2 opacity-65 [&::-webkit-details-marker]:hidden"
onClick={(event) => {
event.preventDefault();
setOpen((current) => !current);
}}
>
<ChevronRight className="h-3 w-3 shrink-0 transition group-open:rotate-90" />
<span className="flex min-w-0 items-center gap-2">
<span className="font-mono text-[9px] font-semibold uppercase tracking-[0.12em]">
@@ -261,24 +276,26 @@ const ThinkingDetailsRow = memo(function ThinkingDetailsRow({
) : null}
</span>
</summary>
<div className="mt-2 space-y-2 pl-5">
{traceEvents.map((event, index) =>
event.kind === "thinking" ? (
<div
key={`thinking-event-${index}-${event.text.slice(0, 48)}`}
className="agent-markdown min-w-0 text-foreground/85"
>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{event.text}</ReactMarkdown>
</div>
) : (
<ToolCallDetails
key={`thinking-tool-${index}-${event.text.slice(0, 48)}`}
line={event.text}
className="rounded-[8px] border border-border/70 bg-surface-3 px-2 py-1 text-[10px] text-muted-foreground"
/>
)
)}
</div>
{open ? (
<div className="mt-2 space-y-2 pl-5">
{traceEvents.map((event, index) =>
event.kind === "thinking" ? (
<div
key={`thinking-event-${index}-${event.text.slice(0, 48)}`}
className="agent-markdown min-w-0 text-foreground/85"
>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{event.text}</ReactMarkdown>
</div>
) : (
<ToolCallDetails
key={`thinking-tool-${index}-${event.text.slice(0, 48)}`}
line={event.text}
className="rounded-[8px] border border-border/70 bg-surface-3 px-2 py-1 text-[10px] text-muted-foreground"
/>
)
)}
</div>
) : null}
</details>
);
});
+23 -17
View File
@@ -265,47 +265,53 @@ const reducer = (state: AgentStoreState, action: Action): AgentStoreState => {
const patch = action.patch;
const nextSessionKey = (patch.sessionKey ?? agent.sessionKey).trim();
const sessionKeyChanged = nextSessionKey !== agent.sessionKey.trim();
const patchHasTranscriptEntries = Array.isArray(patch.transcriptEntries);
const patchHasOutputLines = Array.isArray(patch.outputLines);
const patchMutatesTranscript = patchHasTranscriptEntries || patchHasOutputLines;
const existingEntries = ensureTranscriptEntries(agent);
const base: AgentState = { ...agent, ...patch };
let nextEntries = Array.isArray(base.transcriptEntries)
? [...base.transcriptEntries]
: existingEntries;
let nextOutputLines = Array.isArray(base.outputLines)
? [...base.outputLines]
: [...agent.outputLines];
let nextEntries: TranscriptEntry[] = existingEntries;
if (Array.isArray(base.transcriptEntries)) {
nextEntries = base.transcriptEntries as TranscriptEntry[];
}
let nextOutputLines: string[] = agent.outputLines;
if (Array.isArray(base.outputLines)) {
nextOutputLines = base.outputLines as string[];
}
let transcriptMutated = false;
if (Array.isArray(patch.transcriptEntries)) {
if (patchHasTranscriptEntries) {
const patchedTranscriptEntries = patch.transcriptEntries as TranscriptEntry[];
const normalized = TRANSCRIPT_V2_ENABLED
? sortTranscriptEntries(patch.transcriptEntries)
: [...patch.transcriptEntries];
? sortTranscriptEntries(patchedTranscriptEntries)
: [...patchedTranscriptEntries];
transcriptMutated = !areTranscriptEntriesEqual(existingEntries, normalized);
nextEntries = normalized;
nextOutputLines = buildOutputLinesFromTranscriptEntries(normalized);
} else if (Array.isArray(patch.outputLines)) {
} else if (patchHasOutputLines) {
const patchedOutputLines = patch.outputLines as string[];
const rebuilt = buildTranscriptEntriesFromLines({
lines: patch.outputLines,
lines: patchedOutputLines,
sessionKey: nextSessionKey || agent.sessionKey,
source: "legacy",
startSequence: 0,
confirmed: true,
});
const normalized = TRANSCRIPT_V2_ENABLED ? sortTranscriptEntries(rebuilt) : rebuilt;
transcriptMutated = !areStringArraysEqual(agent.outputLines, patch.outputLines);
transcriptMutated = !areStringArraysEqual(agent.outputLines, patchedOutputLines);
nextEntries = normalized;
nextOutputLines = TRANSCRIPT_V2_ENABLED
? buildOutputLinesFromTranscriptEntries(normalized)
: [...patch.outputLines];
: [...patchedOutputLines];
}
const revision = transcriptMutated
? (agent.transcriptRevision ?? 0) + 1
: (patch.transcriptRevision ?? agent.transcriptRevision ?? 0);
const nextCounter = nextTranscriptSequenceCounter(
base.transcriptSequenceCounter,
nextEntries
);
const nextCounter = patchMutatesTranscript
? nextTranscriptSequenceCounter(base.transcriptSequenceCounter, nextEntries)
: (base.transcriptSequenceCounter ?? agent.transcriptSequenceCounter ?? 0);
return {
...base,
@@ -1,6 +1,6 @@
import { createElement } from "react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, render, screen, within } from "@testing-library/react";
import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
import type { AgentState } from "@/features/agents/state/store";
import { AgentChatPanel } from "@/features/agents/components/AgentChatPanel";
import type { GatewayModelChoice } from "@/lib/gateway/models";
@@ -83,9 +83,11 @@ describe("AgentChatPanel markdown rendering", () => {
expect(screen.queryByText(/^Output$/)).not.toBeInTheDocument();
expect(screen.queryByText("Extract output")).not.toBeInTheDocument();
fireEvent.click(screen.getByText("Thinking (internal)"));
const toolSummary = screen.getByText("SHELL · ok");
const toolDetails = toolSummary.closest("details");
expect(toolDetails).toBeTruthy();
fireEvent.click(toolSummary);
expect(within(toolDetails as HTMLElement).getByText("done")).toBeInTheDocument();
});
@@ -130,6 +132,7 @@ describe("AgentChatPanel markdown rendering", () => {
const thinkingDetails = screen.getByText("Thinking (internal)").closest("details");
expect(thinkingDetails).toBeTruthy();
fireEvent.click(screen.getByText("Thinking (internal)"));
expect(within(thinkingDetails as HTMLElement).getByText(/proposing multi-lane tracking system/i)).toBeInTheDocument();
const memorySearchSummaries = screen.getAllByText(/MEMORY_SEARCH/);
@@ -167,6 +170,7 @@ describe("AgentChatPanel markdown rendering", () => {
})
);
fireEvent.click(screen.getByText("Thinking (internal)"));
expect(screen.getByText("read /tmp/README.md")).toBeInTheDocument();
expect(screen.queryByText("read /tmp/README.md", { selector: "summary" })).toBeNull();
expect(screen.queryByText(/"file_path"/)).toBeNull();
+30
View File
@@ -197,6 +197,36 @@ describe("agent store", () => {
expect(next?.runId).toBeNull();
});
it("keeps_transcript_references_for_non_transcript_agent_updates", () => {
const seed: AgentStoreSeed = {
agentId: "agent-1",
name: "Agent One",
sessionKey: "agent:agent-1:main",
};
let state = agentStoreReducer(initialAgentStoreState, {
type: "hydrateAgents",
agents: [seed],
});
state = agentStoreReducer(state, {
type: "updateAgent",
agentId: "agent-1",
patch: { outputLines: ["> hello", "response"] },
});
const beforeDraftUpdate = state.agents[0];
state = agentStoreReducer(state, {
type: "updateAgent",
agentId: "agent-1",
patch: { draft: "x" },
});
const afterDraftUpdate = state.agents[0];
expect(afterDraftUpdate.outputLines).toBe(beforeDraftUpdate.outputLines);
expect(afterDraftUpdate.transcriptEntries).toBe(beforeDraftUpdate.transcriptEntries);
expect(afterDraftUpdate.transcriptSequenceCounter).toBe(beforeDraftUpdate.transcriptSequenceCounter);
});
it("tracks_unseen_activity_for_non_selected_agents", () => {
const seeds: AgentStoreSeed[] = [
{