mirror of
https://github.com/grp06/openclaw-studio.git
synced 2026-08-14 00:47:51 +00:00
Fix chat input lag
This commit is contained in:
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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[] = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user