合并 master 到 fix/acl-elevation-single-flight
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m30s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m8s
Project CI / Backend tests (pull_request) Successful in 4m8s
Project CI / Frontend tests (pull_request) Successful in 2m10s
Project CI / Native shell tests (pull_request) Successful in 6m13s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m48s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m39s
Project CI / Repository checks (pull_request) Successful in 2m14s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled

- 合入 origin/master(dcb4f2a1c):DirectProject 过程卡读秒粒度与样式修复(#508)
- pitfalls.md 冲突按「两边都保留」解决:ACL 单飞条目置顶,其后保留上游 2026-09-24 读秒条目
- 本次合并未改动 ACL 单飞实现(含刚收口的 key 归一化与冷却基准改动)
This commit is contained in:
2026-09-24 12:08:58 +08:00
6 changed files with 134 additions and 93 deletions
+39 -71
View File
@@ -2705,21 +2705,27 @@ textarea {
background: #fff;
}
/* 运行中状态条陶泥儿正在处理+ 右侧已耗时**单行**
它渲染在消息列表****紧贴输入盒上方见文件末尾 `.project-chat-conversation >
.project-chat-process-card` 那条所以这里只管这一行的几何与配色左右内缩与上下间距
由宿主的对话面板补卡片经历过多行摘要 + 单行标题的旧面板设计留下了 78px 最小高度
9px 行距摘要行在 DirectProject 收敛后已不存在那两个空位只剩空白
保持 grid单列而不是 flexheader 才会被拉满行宽已耗时的 `margin-left: auto` 才有
余量可以吃掉flex header 收缩到内容宽auto 边距恒为 0耗时只能贴着标题 */
.project-chat-process-card {
position: relative;
isolation: isolate;
display: grid;
gap: 9px;
min-height: 78px;
box-sizing: border-box;
margin-top: 12px;
padding: 12px 14px;
border: 1px solid #b9d8c5;
padding: 8px 12px;
border: 1px solid var(--platform-subpanel-border, #e1ccbb);
border-radius: 8px;
background: #f1f8f3;
background: var(--platform-neutral-bg, #fffdfa);
overflow: hidden;
}
/* 扫光用主色而不是白色高光底色是半透明白白色高光在它上面几乎看不见
正在处理于是只剩下一枚脉动点整条状态看着像静止的 */
.project-chat-process-card::after {
content: '';
position: absolute;
@@ -2729,7 +2735,7 @@ textarea {
background: linear-gradient(
90deg,
transparent,
rgb(255 255 255 / 28%),
color-mix(in srgb, var(--platform-accent) 18%, transparent),
transparent
);
pointer-events: none;
@@ -2752,27 +2758,44 @@ textarea {
.project-chat-process-card > header {
display: flex;
min-width: 0;
align-items: center;
gap: 8px;
}
/* 状态点主色 + 同色光晕原先写死绿色`#2f855a`工作台里那条想把它换成主色的覆盖规则
挂在 ` .project-chat-message-list > .project-chat-process-card` 而卡片渲染在列表****
那条规则一直没生效于是暖色面板上挂着一枚绿点 */
.project-chat-process-card > header > span {
width: 8px;
height: 8px;
flex: 0 0 auto;
border-radius: 50%;
background: #2f855a;
box-shadow: 0 0 0 4px rgb(47 133 90 / 14%);
background: var(--platform-accent, #2f855a);
box-shadow: 0 0 0 4px
color-mix(in srgb, var(--platform-accent) 16%, transparent);
animation: project-chat-process-pulse 1.2s ease-in-out infinite;
}
/* 标题是这张卡的唯一主信息它挂在过程层级12px / 次要色这里把它提到正文强色
否则整条状态在浅色底上只剩一片灰 */
.project-chat-process-card > header > strong {
min-width: 0;
overflow: hidden;
color: var(--platform-text-strong);
text-overflow: ellipsis;
white-space: nowrap;
}
.project-chat-process-elapsed {
margin-left: auto !important;
color: inherit;
margin-left: auto;
color: var(--platform-text-base);
font-size: inherit;
font-style: normal;
font-weight: 500;
white-space: nowrap;
/* 100ms 一跳:等宽数字才不会每跳一次就左右抖一下。 */
font-variant-numeric: tabular-nums;
}
@keyframes project-chat-process-pulse {
@@ -2792,37 +2815,6 @@ textarea {
}
}
.project-chat-process-card p {
margin: 0;
color: inherit;
font-size: inherit;
line-height: 1.55;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
max-height: 1.55em;
white-space: pre-wrap;
overflow-wrap: anywhere;
overflow: hidden;
}
.project-chat-process-card p.is-expanded {
display: block;
max-height: 180px;
overflow: auto;
scrollbar-color: rgb(47 111 75 / 58%) transparent;
}
.project-chat-process-card p.is-expanded::-webkit-scrollbar-track,
.project-chat-process-card p.is-expanded::-webkit-scrollbar-corner {
background: transparent;
}
.game-workbench-chat .project-chat-process-card {
border-color: var(--platform-surface-border);
background: var(--platform-warm-bg);
}
.project-chat-surface .agent-runtime-status {
margin: 0;
}
@@ -9303,13 +9295,6 @@ iframe.preview-frame {
color: var(--platform-text-base);
}
.game-workbench-chat
.project-chat-surface.is-direct-codex
.project-chat-message-list
> .project-chat-process-card {
width: 100%;
}
.game-workbench-chat .agent-runtime-status {
max-height: clamp(120px, 24dvh, 240px);
min-height: 0;
@@ -12124,24 +12109,6 @@ button.design-workspace-tree__entry:hover,
white-space: normal;
}
/* 执行过程卡在暖色皮肤下不再用绿色系:改成中性描边 + 暖底,主色只留给状态点与发送钮。 */
.game-workbench-chat
.project-chat-surface.is-direct-codex
.project-chat-message-list
> .project-chat-process-card {
border-color: var(--platform-line-soft);
background: var(--platform-neutral-bg);
}
.game-workbench-chat
.project-chat-surface.is-direct-codex
.project-chat-message-list
> .project-chat-process-card
> header
> span {
background: var(--platform-accent);
}
/* 设置浮层(新增):独立 backdrop + dialog,不再往面板下面追加内容。 */
.project-chat-settings-backdrop {
position: fixed;
@@ -12609,14 +12576,15 @@ button.design-workspace-tree__entry:hover,
justify-content: flex-end;
}
/* 过程卡任务执行中 / 正在思考中现在渲染在消息列表****紧贴输入盒上方固定在
输入框上面不随消息滚走它不再继承消息列表的左右 16px 内缩所以要自己补齐
才能与消息内容输入盒两侧对齐离开列表后列表的 padding-bottom 也不再作用于它 */
/* 过程卡陶泥儿正在处理现在渲染在消息列表****紧贴输入盒上方固定在输入框上面
不随消息滚走它不再继承消息列表的左右 16px 内缩所以要自己补齐才能与消息内容
输入盒两侧对齐离开列表后列表的 padding-bottom 也不再作用于它与最后一条消息的间距
同样由这里给 */
.game-workbench-chat
.project-chat-surface.is-direct-codex
.project-chat-conversation
> .project-chat-process-card {
margin: 0 16px 8px;
margin: 12px 16px 8px;
}
/* 工具调用折叠块块头改成两行第一行图标 + 汇总超长省略第二行状态 + 用时箭头右侧跨两行
@@ -1,11 +1,7 @@
import {
type RefObject,
type UIEventHandler,
useEffect,
useState,
} from 'react';
import type { RefObject, UIEventHandler } from 'react';
import { AgentMessageContent } from '../../../../../../../../packages/shared/src/components/AgentMessageContent';
import { useLiveNow } from '../../../../../features/project-workspace/useLiveNow';
import type { DirectChatTurn } from '../../conversation/directTurnPresentation';
import { formatTurnDuration } from '../ToolCallGroup/toolCallGroupPresentation';
import { DirectProjectTurn } from './DirectProjectTurn';
@@ -82,17 +78,12 @@ export function DirectProjectConversation({
/**
* 运行中回合的已耗时。
*
* 计时器只属于这一小块:秒级 tick 不该把整份回合列表(Markdown、工具组、队列芯片)
* 一起重渲染
* 时钟订在**这一行**上(叶子节点),走对话侧唯一的 `useLiveNow`100ms):耗时文案不足
* 一分钟保留一位小数,秒级 tick 会让那个小数位一秒才动一格,看着像卡住不动
* 100ms 的 tick 也只重建这块文案,不牵动回合列表(Markdown、工具组、队列芯片)。
*/
function DirectTurnElapsed({ startedAt }: { startedAt: number }) {
const [now, setNow] = useState(() => Date.now());
useEffect(() => {
if (startedAt <= 0) return undefined;
setNow(Date.now());
const timer = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(timer);
}, [startedAt]);
const now = useLiveNow(startedAt > 0);
if (startedAt <= 0) return null;
return (
<em className="project-chat-process-elapsed">
@@ -72,7 +72,6 @@ describe('AgentMessageContent', () => {
['.agent-tool-call-group'],
[
'.project-chat-process-card',
'.game-workbench-chat .project-chat-process-card',
'.game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-conversation > .project-chat-process-card',
],
]) {
@@ -6336,7 +6336,7 @@ export function registerProjectWorkbenchFoundationTests() {
expect(styleNumber(editorRule, 'min-height')).toBe(96);
});
it('keeps workbench chat bubbles aligned without shrinking process cards', () => {
it('keeps workbench chat bubbles aligned and the process card inset with them', () => {
const styles = readFileSync(
repoPath('apps/ai-game-creator-shell/src/styles.css'),
'utf8',
@@ -6355,7 +6355,7 @@ export function registerProjectWorkbenchFoundationTests() {
)?.[1] ?? '';
const processCardRules = Array.from(
styles.matchAll(
/\.game-workbench-chat\s+\.project-chat-surface\.is-direct-codex\s+\.project-chat-message-list\s*>\s*\.project-chat-process-card\s*\{([^}]*)\}/gs,
/\.game-workbench-chat\s+\.project-chat-surface\.is-direct-codex\s+\.project-chat-conversation\s*>\s*\.project-chat-process-card\s*\{([^}]*)\}/gs,
),
(match) => match[1],
);
@@ -6371,10 +6371,10 @@ export function registerProjectWorkbenchFoundationTests() {
expect(userMessageRule).toContain('border-radius: 14px 14px 4px;');
expect(userMessageRule).toContain('background: var(--platform-warm-bg);');
expect(userMessageRule).toContain('color: var(--platform-text-base);');
// 执行过程卡有两条同选择器规则:第一条是几何(`width: 100%`),后面那条是 Codex 暖色
// 皮肤下的配色(把基础规则的绿系换成中性描边 + 暖底)。承重的是几何那条
expect(processCardRules.length).toBeGreaterThanOrEqual(1);
expect(processCardRules[0]).toContain('width: 100%;');
// 过程卡渲染在消息列表的**兄弟**位置(列表外,紧贴输入盒上方),拿不到列表的
// `padding: 14px 16px 0`,左右内缩与上下间距只能自己给:左右必须与列表的 16px 对齐
expect(processCardRules.length).toBe(1);
expect(processCardRules[0]).toContain('margin: 12px 16px 8px;');
});
it('enables the run presentation and renders registered images in the resource viewer', async () => {
@@ -0,0 +1,75 @@
// @vitest-environment jsdom
import { cleanup, render } from '@testing-library/react';
import { act, createRef } from 'react';
import { afterEach, expect, test, vi } from 'vitest';
import { DirectProjectConversation } from '../src/view/project-development/chat/components/DirectProjectConversation/DirectProjectConversation';
afterEach(() => {
cleanup();
});
const STARTED_AT = 1_800_000_000_000;
/**
* 读秒粒度:走对话侧唯一的 `useLiveNow``LIVE_TIMER_TICK_MS = 100`)。
*
* 状态条的耗时文案不足一分钟保留一位小数,刷新就必须是 100ms——按 1 秒一跳时,用户看到的
* 是一块「带小数却一格一格跳」的表,像卡住不动。这里用假时钟钉住粒度:把时钟改回 1000ms
* 时第二步即红。
*/
test('运行中状态条的读秒按 100ms 刷新:不足一分钟的耗时以 0.1 秒递增', () => {
vi.useFakeTimers();
try {
vi.setSystemTime(STARTED_AT);
const view = render(
<DirectProjectConversation
turns={[]}
messagesRef={createRef<HTMLDivElement>()}
historyHasMore={false}
nativeRunning
activeTurnStartedAt={STARTED_AT}
onLoadEarlierHistory={() => undefined}
onScroll={() => undefined}
/>,
);
const elapsed = () => view.getByText(/^ /u).textContent;
expect(elapsed()).toBe('已耗时 0.0秒');
act(() => {
vi.advanceTimersByTime(100);
});
expect(elapsed()).toBe('已耗时 0.1秒');
// 1 秒一跳的实现会停在上面的 0.1 秒:再走 400ms 必须继续往上加。
act(() => {
vi.advanceTimersByTime(400);
});
expect(elapsed()).toBe('已耗时 0.5秒');
} finally {
vi.useRealTimers();
}
});
/** 回合不在跑时不订阅时钟:否则空转的 tick 会持续重建状态条所在的那块视图。 */
test('没有运行中的回合时不订阅时钟', () => {
vi.useFakeTimers();
const spy = vi.spyOn(globalThis, 'setInterval');
try {
render(
<DirectProjectConversation
turns={[]}
messagesRef={createRef<HTMLDivElement>()}
historyHasMore={false}
nativeRunning={false}
activeTurnStartedAt={0}
onLoadEarlierHistory={() => undefined}
onScroll={() => undefined}
/>,
);
expect(spy).not.toHaveBeenCalled();
} finally {
spy.mockRestore();
vi.useRealTimers();
}
});
@@ -9,6 +9,14 @@
- **验证**`src-tauri/src/tests/acl_repair_gate.rs`(并发只执行一次、冷却复用、拒绝冷却、清除后可重试、follower 超时、leader panic 唤醒等待者、冷却基准、路径写法归一)。真机复现(无需提权交互即可计数):在 Managed 放行范围内建 8 个带 `.agent/manifest.json` 的假项目 → 对共同祖先的**父目录** `icacls <父目录> /deny *<sid>:(OI)(CI)(RX)` → 挂载启动页,同时数 `powershell.exe` 里命令行带 `RunAs` 的进程数(`Start-Process -Wait` 会让它一直存活到用户应答)与 `consent.exe` 峰值:修复前 8 个并发请求,修复后 1 个;把同一目录的 `\\?\C:\...``C:\...` 两种写法一起塞进最近项目,还能验证 key 归一化是否生效(修复前 2 个、修复后 1 个)。
- **关联**`src-tauri/src/acl_repair_gate.rs``src-tauri/src/config.rs`、issue #498
## 2026-09-24 对话过程卡的读秒退回 1 秒一跳:刷新粒度必须与显示精度同格
- **现象**AGC DirectProject 对话区底部那条「陶泥儿正在处理 / 已耗时 12.4秒」的状态条,小数位一秒才动一格,看着像读数卡住;同一屏里工具卡片的耗时与资源生成侧栏的读秒都在正常走 0.1 秒,只有这一处不动。
- **原因**:耗时文案不足一分钟保留一位小数(`formatElapsedDuration`),刷新就必须是 100ms。这次改动方向本身是对的——把 clock 从整个聊天视图下移到耗时那一行,但顺手在组件里另写了一份 `setInterval(..., 1000)`,绕过了对话侧唯一的 `useLiveNow``LIVE_TIMER_TICK_MS = 100`);`team-conventions.md` 里「运行时用 100ms 叶子时钟刷新一位小数、终态冻结」这条约定当时已经写好,改动没有对齐它。
- **处理(现行口径)**:耗时文案的刷新一律走 `useLiveNow`,不在视图组件里另起 interval;tick 只订在显示耗时的那一行(叶子节点),不能落在整块面板或整份回合列表上。可机检的判据是「不足一分钟的耗时必须每 100ms 递增一次小数位」。
- **验证**`npx vitest run apps/ai-game-creator-shell/tests/directProjectProcessStatus.test.tsx`(把 `LIVE_TIMER_TICK_MS` 临时改回 1000 时第一步即红);真实浏览器里 600ms 内文案从 `18.0秒` 走到 `18.6秒`
- **关联**`apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectConversation/DirectProjectConversation.tsx``apps/ai-game-creator-shell/src/features/project-workspace/useLiveNow.ts``apps/ai-game-creator-shell/src/styles.css`
## 策划回复的重复终态不能重新启动伪流式
策划 Runtime 会通过状态事件与命令返回交付同一份最终视图。若前端清空临时正文后再拿“最后一条非用户历史消息”回填动画,就会出现正式回复旁又播放一遍、播放后消失的假重试。正文应按 `messageId` 保存显示进度,与正式消息共用一个气泡;请求完成不清动画,不延迟正式业务状态。Provider 自动重试复用消息 ID 并发送空文本,只允许重置未持久化的该条回复。正文、工具状态和 reasoning 分开;事件与异步命令收尾均检查项目及活动回合,旧请求不能覆盖新回合。详见 [AGC 实施计划](../../technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md)。