优化 Direct 流式回复展示

将生成中的累计正文直接显示在助手会话气泡中。

过程卡只保留阶段与执行信息,避免工具事件覆盖正文。

展开详情的滚动条轨道与角落改为透明。

补充流式、失败清理与正式消息接管的界面回归验证。
This commit is contained in:
2026-09-02 17:50:17 +08:00
parent 8f7017f6c0
commit c668964d2f
5 changed files with 54 additions and 28 deletions
+26 -11
View File
@@ -335,11 +335,7 @@ function directCodexProcessDetail({
return '正在记录失败原因';
}
if (status === 'streaming') {
const text = accumulatedText?.trim();
if (text) {
return `正在生成回复:${text}`;
}
return directCodexActivityDetail(activity, status);
return '正在生成回复';
}
if (status === 'finalizing') {
return directCodexActivityDetail(activity, status);
@@ -351,6 +347,24 @@ function directCodexProcessDetail({
return directCodexActivityDetail(activity, status);
}
function directCodexTransientReplyText({
accumulatedText,
status,
}: {
accumulatedText?: string | null;
status: string;
}) {
if (
status !== 'streaming' &&
status !== 'finalizing' &&
status !== 'completed'
) {
return null;
}
const text = accumulatedText?.trim();
return text || null;
}
function directCodexConversationMessageId(
turnId: string,
role: ChatMessage['role'],
@@ -1305,15 +1319,18 @@ export function App({
setDirectCodexStatus(payload.status);
setDirectCodexProgress(processDetail);
setDirectCodexProgressUpdatedAt(updatedAt);
setDirectCodexTransientReply(processDetail);
setDirectCodexTransientReply('');
setDirectCodexTransientReplyUpdatedAt(null);
return;
}
setDirectCodexStatus(payload.status);
setDirectCodexProgress(processDetail);
setDirectCodexProgressUpdatedAt(updatedAt);
setDirectCodexTransientReply(processDetail);
setDirectCodexTransientReplyUpdatedAt(updatedAt);
const transientReply = directCodexTransientReplyText(payload);
if (transientReply !== null) {
setDirectCodexTransientReply(transientReply);
setDirectCodexTransientReplyUpdatedAt(updatedAt);
}
},
)
.then((unlisten) => {
@@ -1358,7 +1375,6 @@ export function App({
const progressDetail = ensureDirectProcessPrefix(event.payload.message);
setDirectCodexStatus('running');
setDirectCodexProgress(progressDetail);
setDirectCodexTransientReply(progressDetail);
setDirectCodexProgressUpdatedAt(Date.now());
return;
}
@@ -5537,7 +5553,7 @@ export function App({
setDirectCodexStatus('accepted');
setDirectCodexProcessKey(`${directProjectPath}\u0000${clientTurnId}`);
setDirectCodexProgress('正在等待陶泥儿开始');
setDirectCodexTransientReply('正在等待陶泥儿开始');
setDirectCodexTransientReply('');
setDirectCodexProgressUpdatedAt(Date.now());
setDirectCodexTransientReplyUpdatedAt(null);
setProjectSupervisorRuntimeError('');
@@ -5609,7 +5625,6 @@ export function App({
);
setDirectCodexStatus('finalizing');
setDirectCodexProgress('正在同步项目文件');
setDirectCodexTransientReply('正在同步项目文件');
setDirectCodexProgressUpdatedAt(Date.now());
await refreshDirectProjectManifest(directProjectPath);
}
@@ -186,7 +186,7 @@ export function ProjectSupervisorView({
</p>
))}
{directCodex &&
(runtimePanelProps.controlBusy || Boolean(transientReply)) ? (
(runtimePanelProps.controlBusy || Boolean(directProcessDetail)) ? (
<section
className={`project-supervisor-process-card${runtimePanelProps.controlBusy ? ' is-active' : ''}`}
aria-label="陶泥儿执行过程"
@@ -203,7 +203,7 @@ export function ProjectSupervisorView({
: '陶泥儿正在处理'}
</strong>
</header>
{(directCodex ? directProcessDetail : transientReply) ? (
{directProcessDetail ? (
<div className="project-supervisor-process-detail">
<p
className={
@@ -211,14 +211,10 @@ export function ProjectSupervisorView({
}
aria-label="陶泥儿正在执行的内容"
>
{directCodex ? directProcessDetail : transientReply}
{directProcessDetail}
</p>
{(directCodex
? directProcessDetail
: transientReply
).includes('\n') ||
(directCodex ? directProcessDetail : transientReply).length >
96 ? (
{directProcessDetail.includes('\n') ||
directProcessDetail.length > 96 ? (
<button
type="button"
className="project-supervisor-process-toggle"
@@ -237,10 +233,13 @@ export function ProjectSupervisorView({
</div>
) : null}
</section>
) : transientReply ? (
) : null}
{transientReply ? (
<p
className="message message--assistant"
aria-label="项目总控 Agent 实时回复"
aria-label={
directCodex ? '陶泥儿实时回复' : '项目总控 Agent 实时回复'
}
aria-live="polite"
data-runtime-owned="true"
>
@@ -2587,6 +2587,12 @@ textarea {
display: block;
max-height: 180px;
overflow: auto;
scrollbar-color: rgb(47 111 75 / 58%) transparent;
}
.project-supervisor-process-card p.is-expanded::-webkit-scrollbar-track,
.project-supervisor-process-card p.is-expanded::-webkit-scrollbar-corner {
background: transparent;
}
.project-supervisor-process-detail {
@@ -5700,7 +5700,10 @@ export function registerProjectSupervisorSurfaceTests() {
expect(
within(streamingProcessCard).getByLabelText('陶泥儿正在执行的内容')
.textContent,
).toContain('正在生成回复DIRECT_STREAM:先完成正式客户端玩法拆解');
).toBe('正在生成回复');
expect(
within(supervisorSurface).getByLabelText('陶泥儿实时回复').textContent,
).toBe('DIRECT_STREAM:先完成正式客户端玩法拆解');
expect(directMessageList.scrollTop).toBe(640);
expect(within(supervisorSurface).queryByText(//u)).toBeNull();
expect(
@@ -5725,6 +5728,9 @@ export function registerProjectSupervisorSurfaceTests() {
expect(
within(streamingProcessCard).getByText(/ npm test/u),
).not.toBeNull();
expect(
within(supervisorSurface).getByLabelText('陶泥儿实时回复').textContent,
).toBe('DIRECT_STREAM:先完成正式客户端玩法拆解');
expect(
(
within(streamingProcessCard).getByRole('button', {
@@ -5808,10 +5814,10 @@ export function registerProjectSupervisorSurfaceTests() {
expect(
within(failedTurnProcessCard).getByLabelText('陶泥儿正在执行的内容')
.textContent,
).toContain('正在生成回复:即将失败的临时正文');
).toBe('正在生成回复');
expect(
within(supervisorSurface).queryByLabelText('陶泥儿实时回复'),
).toBeNull();
within(supervisorSurface).getByLabelText('陶泥儿实时回复').textContent,
).toBe('即将失败的临时正文');
await act(async () => {
directTurnUpdateHandler?.({
payload: {
@@ -18,9 +18,9 @@
## 2026-09-02 Direct 过程卡按回合阶段状态驱动
- 背景:DirectProject 结果卡把工具活动词、中间文本和真实回复增量都当成“实时回复”,标题随最近一次事件跳动;上游常整包返回正文时还叠加合成打字机,用户看到的是行为名而非当前阶段。
- 决策:Direct 过程卡顶部标题只由 `GameCreatorDirectTurnUpdateStatus` 决定(accepted=需求已接收 / running=任务执行中 / streaming=回复生成中 / finalizing=结果整理中 / completed=回复已生成 / failed=处理失败),小字只展示当前正在执行的具体内容并统一加“正在”前缀;真实回复增量(AccumulatedText)才标记 streaming,计划、推理、工具输出与 Activity 一律 running。移除合成打字机回放;工具说明/中间文本不再触发 streaming。展开/收起是同一 `project + clientTurnId` 内的持久状态,内容更新不重置,切换新回合才收起。
- 决策:Direct 过程卡顶部标题只由 `GameCreatorDirectTurnUpdateStatus` 决定(accepted=需求已接收 / running=任务执行中 / streaming=回复生成中 / finalizing=结果整理中 / completed=回复已生成 / failed=处理失败),小字只展示当前正在执行的具体内容并统一加“正在”前缀;真实回复增量(AccumulatedText)才标记 streaming,计划、推理、工具输出与 Activity 一律 running。生成中的累计回复直接作为 assistant 消息气泡在会话列表中原位更新,不再拼进过程卡;进入 finalizing / completed 时保留完整累计回复直到正式消息接管,失败时清除未完成正文。移除合成打字机回放;工具说明/中间文本不再触发 streaming。展开/收起是同一 `project + clientTurnId` 内的持久状态,内容更新不重置,切换新回合才收起;展开详情的滚动条轨道和角落保持透明
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/agent/direct_runtime.rs` 的 DirectProject observer、`apps/ai-game-creator-shell/src/App.tsx` 的事件投影、`ProjectSupervisorView` 过程卡渲染与对应 AppSurface 回归。
- 验证方式:Rust 单测证明只有开启流式时的 AccumulatedText 是 streamingAppSurface 覆盖接受态、running 长文本展开、streaming 标题与小字、同一回合后续 running 不收起、失败态小字与卡片清理AGC typecheck、全量 appSurface、rustfmt、`npm run check:encoding``git diff --check` 通过。
- 验证方式:Rust 单测证明只有开启流式时的 AccumulatedText 是 streamingAppSurface 覆盖接受态、running 长文本展开、streaming 正文进入 assistant 气泡且过程卡只显示阶段、同一回合后续 running 不覆盖正文也不收起、失败后清除未完成正文、正式消息接管不重复;样式核对确认展开详情的滚动条轨道与角落透明AGC typecheck、全量 appSurface、rustfmt、`npm run check:encoding``git diff --check` 通过。
- 关联文档:`docs/technical/【技术方案】Direct回合行为审计账本-2026-08-31.md`、分支 `feat/agc-llm-router-official-chain`
---