优化 Direct 流式回复展示
将生成中的累计正文直接显示在助手会话气泡中。 过程卡只保留阶段与执行信息,避免工具事件覆盖正文。 展开详情的滚动条轨道与角落改为透明。 补充流式、失败清理与正式消息接管的界面回归验证。
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
+10
-11
@@ -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 是 streaming;AppSurface 覆盖接受态、running 长文本展开、streaming 标题与小字、同一回合后续 running 不收起、失败态小字与卡片清理;AGC typecheck、全量 appSurface、rustfmt、`npm run check:encoding`、`git diff --check` 通过。
|
||||
- 验证方式:Rust 单测证明只有开启流式时的 AccumulatedText 是 streaming;AppSurface 覆盖接受态、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`。
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user