Improve WebUI streaming and Python tooling
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user