实时回合按顺序交替渲染正文与工具块
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 1m40s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 2m25s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 2m26s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 2m22s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m57s
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / Frontend tests (pull_request) Failing after 3m14s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m29s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m14s
Project CI / Native shell tests (pull_request) Successful in 7m29s
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 1m40s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 2m25s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 2m26s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 2m22s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m57s
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / Frontend tests (pull_request) Failing after 3m14s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m29s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m14s
Project CI / Native shell tests (pull_request) Successful in 7m29s
- App.tsx:记录"每个工具首次出现时已生成正文的长度"(切分点)并在收尾/清空对话时重置 - ProjectSupervisorView:新增 renderLiveStream,按切分点把流式正文切成若干段,工具块就地插在对应位置;同一切分点的工具合并成一个块 - 有正文时不再重复渲染独立实时工具组(避免同一批工具出现两次) - 历史回合没有切分点:退化为"正文 → 工具块 → 轮次统计",老项目与旧数据不受影响 - tsc 通过;未跑测试
This commit is contained in:
@@ -883,6 +883,14 @@ export function App({
|
||||
// 直连回合的思考过程(流式):整段替换;回合结束/开始新回合/清空对话时一并清掉。
|
||||
const [directCodexTransientReasoning, setDirectCodexTransientReasoning] =
|
||||
useState('');
|
||||
/** 实时回合里"某个工具首次出现时,已生成正文的长度"——用它把正文与工具交替排列。 */
|
||||
const [directToolTextOffsets, setDirectToolTextOffsets] = useState<
|
||||
Record<string, Record<string, number>>
|
||||
>({});
|
||||
const directToolTextOffsetsRef = useRef<Record<string, Record<string, number>>>(
|
||||
{},
|
||||
);
|
||||
const directAccumulatedTextLengthRef = useRef(0);
|
||||
const [
|
||||
directCodexTransientReplyUpdatedAt,
|
||||
setDirectCodexTransientReplyUpdatedAt,
|
||||
@@ -984,6 +992,9 @@ export function App({
|
||||
setDirectCodexProgressUpdatedAt(null);
|
||||
setDirectCodexTransientReply('');
|
||||
setDirectCodexTransientReasoning('');
|
||||
directToolTextOffsetsRef.current = {};
|
||||
setDirectToolTextOffsets({});
|
||||
directAccumulatedTextLengthRef.current = 0;
|
||||
directCodexTransientReplyRef.current = '';
|
||||
setDirectCodexTransientReplyUpdatedAt(null);
|
||||
}
|
||||
@@ -1083,6 +1094,9 @@ export function App({
|
||||
setDirectCodexProgressUpdatedAt(Date.now());
|
||||
setDirectCodexTransientReply('');
|
||||
setDirectCodexTransientReasoning('');
|
||||
directToolTextOffsetsRef.current = {};
|
||||
setDirectToolTextOffsets({});
|
||||
directAccumulatedTextLengthRef.current = 0;
|
||||
directCodexTransientReplyRef.current = '';
|
||||
setDirectCodexTransientReplyUpdatedAt(null);
|
||||
setProjectSupervisorRuntimeError('');
|
||||
@@ -1117,6 +1131,9 @@ export function App({
|
||||
activeDirectCodexTurnRef.current = null;
|
||||
setDirectCodexTransientReply('');
|
||||
setDirectCodexTransientReasoning('');
|
||||
directToolTextOffsetsRef.current = {};
|
||||
setDirectToolTextOffsets({});
|
||||
directAccumulatedTextLengthRef.current = 0;
|
||||
directCodexTransientReplyRef.current = '';
|
||||
setDirectCodexTransientReplyUpdatedAt(null);
|
||||
return true;
|
||||
@@ -2188,7 +2205,29 @@ export function App({
|
||||
);
|
||||
}
|
||||
// 工具调用增量:字段可选,老事件(undefined)走原路径,行为不变。
|
||||
if (typeof payload.accumulatedText === 'string') {
|
||||
directAccumulatedTextLengthRef.current = payload.accumulatedText.length;
|
||||
}
|
||||
if (payload.toolCalls?.length) {
|
||||
// 每个工具只记第一次出现时的正文长度:这就是它在对话流里的位置。
|
||||
const turnOffsets =
|
||||
directToolTextOffsetsRef.current[payload.turnId] ?? {};
|
||||
let offsetsChanged = false;
|
||||
for (const call of payload.toolCalls) {
|
||||
const callId = (call as { id?: string }).id;
|
||||
if (!callId || turnOffsets[callId] !== undefined) {
|
||||
continue;
|
||||
}
|
||||
turnOffsets[callId] = directAccumulatedTextLengthRef.current;
|
||||
offsetsChanged = true;
|
||||
}
|
||||
if (offsetsChanged) {
|
||||
directToolTextOffsetsRef.current = {
|
||||
...directToolTextOffsetsRef.current,
|
||||
[payload.turnId]: { ...turnOffsets },
|
||||
};
|
||||
setDirectToolTextOffsets(directToolTextOffsetsRef.current);
|
||||
}
|
||||
applyDirectToolCalls(
|
||||
payload.toolCalls.map((call) => ({
|
||||
...call,
|
||||
@@ -2212,6 +2251,9 @@ export function App({
|
||||
setDirectCodexProgressUpdatedAt(updatedAt);
|
||||
setDirectCodexTransientReply('');
|
||||
setDirectCodexTransientReasoning('');
|
||||
directToolTextOffsetsRef.current = {};
|
||||
setDirectToolTextOffsets({});
|
||||
directAccumulatedTextLengthRef.current = 0;
|
||||
setDirectCodexTransientReplyUpdatedAt(null);
|
||||
return;
|
||||
}
|
||||
@@ -6860,6 +6902,9 @@ export function App({
|
||||
setDirectCodexProgress('正在等待陶泥儿开始');
|
||||
setDirectCodexTransientReply('');
|
||||
setDirectCodexTransientReasoning('');
|
||||
directToolTextOffsetsRef.current = {};
|
||||
setDirectToolTextOffsets({});
|
||||
directAccumulatedTextLengthRef.current = 0;
|
||||
setDirectCodexProgressUpdatedAt(Date.now());
|
||||
directCodexTransientReplyRef.current = '';
|
||||
setDirectCodexTransientReplyUpdatedAt(null);
|
||||
@@ -12594,6 +12639,7 @@ export function App({
|
||||
if (projectSupervisorOnly) {
|
||||
return (
|
||||
<ProjectSupervisorView
|
||||
directToolTextOffsets={directToolTextOffsets}
|
||||
transientReasoning={directCodexTransientReasoning}
|
||||
initialSupervisorMessage={initialSupervisorMessage}
|
||||
activeVersionId={chatActiveVersionId}
|
||||
|
||||
+72
-2
@@ -159,6 +159,8 @@ type ProjectSupervisorViewProps = RuntimePanelProps & {
|
||||
transientReply: string;
|
||||
/** 流式思考过程(direct-codex):拿不到就为空,空则不渲染。 */
|
||||
transientReasoning?: string;
|
||||
/** 实时回合里"某工具首次出现时正文的长度":用于把正文与工具按顺序交替渲染。 */
|
||||
directToolTextOffsets?: Record<string, Record<string, number>>;
|
||||
showDesignReasoning?: boolean;
|
||||
designReasoning?: string;
|
||||
designReasoningEntries?: DesignReasoningEntry[];
|
||||
@@ -227,6 +229,7 @@ export function ProjectSupervisorView({
|
||||
showProfessionalCollaboration = true,
|
||||
transientReply,
|
||||
transientReasoning = '',
|
||||
directToolTextOffsets = {},
|
||||
showDesignReasoning = false,
|
||||
designReasoning = '',
|
||||
designReasoningEntries = [],
|
||||
@@ -371,6 +374,69 @@ export function ProjectSupervisorView({
|
||||
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 把实时正文与工具块按"工具出现时正文长度"交替排列:
|
||||
* 文本段 → 该位置的工具块 → 下一段文本……同一切分点的工具合并成一个块。
|
||||
* 没有切分点(历史回合)时退化为"正文在前、工具块在后",与老项目兼容。
|
||||
*/
|
||||
const renderLiveStream = (liveText: string, calls: typeof liveToolCalls) => {
|
||||
const offsets = directToolTextOffsets[liveToolCallTurnId] ?? {};
|
||||
const groups = new Map<number, typeof calls>();
|
||||
for (const call of calls) {
|
||||
const offset = offsets[call.id];
|
||||
const key = Number.isFinite(offset) ? Number(offset) : liveText.length;
|
||||
const bucket = groups.get(key) ?? [];
|
||||
bucket.push(call);
|
||||
groups.set(key, bucket);
|
||||
}
|
||||
if (groups.size === 0) {
|
||||
return null;
|
||||
}
|
||||
const positions = [...groups.keys()].sort((left, right) => left - right);
|
||||
const parts: ReactNode[] = [];
|
||||
let cursor = 0;
|
||||
for (const position of positions) {
|
||||
const segment = liveText.slice(cursor, Math.max(cursor, position));
|
||||
if (segment.length > 0) {
|
||||
parts.push(
|
||||
<div
|
||||
key={`live-text-${position}`}
|
||||
className="message message--assistant"
|
||||
aria-live="polite"
|
||||
data-runtime-owned="true"
|
||||
>
|
||||
<ChatMarkdownMessage role="assistant" text={segment} streaming />
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
const bucket = groups.get(position) ?? [];
|
||||
parts.push(
|
||||
<ToolCallGroup
|
||||
key={`live-tools-${position}`}
|
||||
calls={bucket}
|
||||
userSentAt={userMessageUpdatedAtForTurn(liveToolCallTurnId)}
|
||||
active
|
||||
className="message-tool-call"
|
||||
/>,
|
||||
);
|
||||
cursor = Math.max(cursor, position);
|
||||
}
|
||||
const tail = liveText.slice(cursor);
|
||||
if (tail.length > 0) {
|
||||
parts.push(
|
||||
<div
|
||||
key="live-text-tail"
|
||||
className="message message--assistant"
|
||||
aria-live="polite"
|
||||
data-runtime-owned="true"
|
||||
>
|
||||
<ChatMarkdownMessage role="assistant" text={tail} streaming />
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
return parts;
|
||||
};
|
||||
|
||||
/** 整轮会话的结束时间与耗时(进行中时用 tick 驱的 now,所以秒数会实时跳动)。 */
|
||||
const renderTurnUsage = (turnId: string) => {
|
||||
if (!turnId) {
|
||||
@@ -573,7 +639,10 @@ export function ProjectSupervisorView({
|
||||
) : null}
|
||||
{/* 直连回合的流式正文:恢复为对话区里的普通 assistant 消息(不再放进状态卡片),
|
||||
这样"边生成边显示"和"状态卡片只放状态"两件事同时成立。 */}
|
||||
{directCodex && transientReply ? (
|
||||
{directCodex && transientReply && liveToolCalls.length > 0
|
||||
? renderLiveStream(transientReply, liveToolCalls)
|
||||
: null}
|
||||
{directCodex && transientReply && liveToolCalls.length === 0 ? (
|
||||
<div
|
||||
className="message message--assistant"
|
||||
aria-label="陶泥儿实时回复"
|
||||
@@ -587,7 +656,8 @@ export function ProjectSupervisorView({
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{liveToolCalls.length > 0 ? (
|
||||
{/* 有正文时,工具块的顺序由 renderLiveStream 决定,这里不再重复渲染 */}
|
||||
{liveToolCalls.length > 0 && !(directCodex && transientReply) ? (
|
||||
<ToolCallGroup
|
||||
calls={liveToolCalls}
|
||||
userSentAt={userMessageUpdatedAtForTurn(liveToolCallTurnId)}
|
||||
|
||||
Reference in New Issue
Block a user