Improve WebUI streaming and Python tooling

This commit is contained in:
武阳
2026-05-06 20:37:17 +08:00
parent a0da3c2423
commit 9ca675be0d
13 changed files with 930 additions and 147 deletions
+98 -12
View File
@@ -1061,15 +1061,30 @@ const ActivityChainSummary: FC<{
partIndex: number | undefined;
}> = ({ messageId, partIndex }) => {
const { openItem } = useActivityPanel();
const startedAtRef = useRef(Date.now());
const [elapsedMs, setElapsedMs] = useState(0);
const label = useAuiState((s) => {
const message = s.message;
return summarizeActivityChainLabel(message.content, message.status?.type);
return summarizeActivityChainLabel(
message.content,
message.status?.type,
elapsedMs,
);
});
const active = useAuiState((s) => {
const message = s.message;
return message.status?.type === "running";
});
useEffect(() => {
if (!active) return;
setElapsedMs(Date.now() - startedAtRef.current);
const timer = window.setInterval(() => {
setElapsedMs(Date.now() - startedAtRef.current);
}, 1000);
return () => window.clearInterval(timer);
}, [active]);
return (
<button
type="button"
@@ -1085,14 +1100,22 @@ const ActivityChainSummary: FC<{
);
};
type ActivitySummaryPart = {
type: string;
text?: string;
result?: unknown;
args?: unknown;
toolName?: string;
};
function summarizeActivityChainLabel(
content: readonly { type: string; text?: string; result?: unknown }[],
content: readonly ActivitySummaryPart[],
status: string | undefined,
elapsedMs = 0,
) {
const toolCount = content.filter((part) => part.type === "tool-call").length;
const runningToolCount = content.filter(
(part) => part.type === "tool-call" && part.result === undefined,
).length;
const toolParts = content.filter((part) => part.type === "tool-call");
const runningTool = toolParts.findLast((part) => part.result === undefined);
const activeTool = runningTool ?? toolParts.at(-1);
const reasoningText =
content.find((part) => part.type === "reasoning")?.text ?? "";
const lastReasoningLine = reasoningText
@@ -1101,19 +1124,64 @@ function summarizeActivityChainLabel(
.map((line) => line.trim())
.filter(Boolean)
.at(-1);
const stageNote = activeTool ? getToolStageNote(activeTool.args) : "";
const activeDetail =
stageNote ||
(activeTool?.toolName ? `调用 ${activeTool.toolName}` : "") ||
(isGenericActivityLine(lastReasoningLine) ? "" : lastReasoningLine);
if (status === "running") {
return runningToolCount > 0 ? "调用工具中" : "思考中";
const elapsed = `用时 ${formatActivityDuration(elapsedMs)}`;
if (activeDetail) {
return `${compactActivityLabel(activeDetail, 15)}${elapsed}`;
}
return `思考中,${elapsed}`;
}
const label = compactActivityLabel(lastReasoningLine || "思考完成");
return toolCount > 0 ? `${label} · 调用了 ${toolCount} 个工具` : label;
const label = compactActivityLabel(
activeDetail || lastReasoningLine || "思考完成",
15,
);
const finalDuration = extractDurationLabel(lastReasoningLine);
const elapsed = finalDuration || formatActivityDuration(elapsedMs);
return `${label},用时 ${elapsed}`;
}
function compactActivityLabel(label: string) {
function getToolStageNote(value: unknown) {
if (!value || typeof value !== "object" || Array.isArray(value)) return "";
const note = (value as Record<string, unknown>).__claw_stage_note;
return typeof note === "string" ? note.trim() : "";
}
function isGenericActivityLine(value?: string) {
if (!value) return true;
return (
value.startsWith("思考中") ||
value.startsWith("思考并执行完成") ||
value.endsWith("输出中...")
);
}
function extractDurationLabel(value?: string) {
if (!value) return "";
const match = value.match(/用时\s*([^。,.\s]+)/);
return match?.[1] ?? "";
}
function formatActivityDuration(ms: number) {
const safeMs = Math.max(0, Math.round(ms));
if (safeMs < 1000) return `${safeMs}ms`;
const totalSeconds = Math.round(safeMs / 1000);
if (totalSeconds < 60) return `${totalSeconds}s`;
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return seconds > 0 ? `${minutes}min${seconds}s` : `${minutes}min`;
}
function compactActivityLabel(label: string, maxLength = 15) {
const normalized = label.replace(/\s+/g, " ").trim();
if (normalized.length <= 36) return normalized;
return `${normalized.slice(0, 35)}`;
if (normalized.length <= maxLength) return normalized;
return `${normalized.slice(0, Math.max(1, maxLength - 1))}`;
}
const AssistantActionBar: FC = () => {
@@ -1251,10 +1319,13 @@ const ImeComposerInput: FC<ComponentProps<typeof TextareaAutosize>> = ({
const [localText, setLocalText] = useState(storeText);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const isComposingRef = useRef(false);
const justSubmittedRef = useRef(false);
const isDisabled = runtimeDisabled || disabled;
useEffect(() => {
if (!isComposingRef.current) {
if (justSubmittedRef.current && storeText) return;
if (!storeText) justSubmittedRef.current = false;
setLocalText(storeText);
}
}, [storeText]);
@@ -1277,6 +1348,19 @@ const ImeComposerInput: FC<ComponentProps<typeof TextareaAutosize>> = ({
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
}, [autoFocus, isDisabled]);
useEffect(() => {
const clearAfterSubmit = () => {
justSubmittedRef.current = true;
setLocalText("");
};
const unsubscribeComposer = aui.on("composer.send", clearAfterSubmit);
const unsubscribeRun = aui.on("thread.runStart", clearAfterSubmit);
return () => {
unsubscribeComposer();
unsubscribeRun();
};
}, [aui]);
const syncComposerText = useCallback(
(value: string) => {
if (!aui.composer().getState().isEditing) return;
@@ -1324,6 +1408,7 @@ const ImeComposerInput: FC<ComponentProps<typeof TextareaAutosize>> = ({
event.preventDefault();
syncComposerText(localText);
aui.composer().send();
justSubmittedRef.current = true;
setLocalText("");
};
@@ -1336,6 +1421,7 @@ const ImeComposerInput: FC<ComponentProps<typeof TextareaAutosize>> = ({
onChange={(event) => {
onChange?.(event);
const nextText = event.currentTarget.value;
justSubmittedRef.current = false;
setLocalText(nextText);
const isNativeComposing =
(event.nativeEvent as { isComposing?: boolean }).isComposing === true;