调整 Agent 对话栏 Logo 与工作台布局 #183

Merged
kdletters merged 3 commits from codex/agent-chat-layout-logo into master 2026-08-23 22:09:47 +08:00
7 changed files with 243 additions and 69 deletions
@@ -307,6 +307,7 @@ export function WorkspaceLauncherShell({
preview={activeProjectPreview}
agentRuntimeSummaries={activeProjectAgentRuntimeSummaries}
agentResults={activeProjectAgentResults}
walletEntry={<AccountWalletBar controller={accountWallet} />}
onPlay={() =>
requestCurrentProjectPlay(currentProjectContext.projectPath)
}
+96 -30
View File
@@ -4923,11 +4923,14 @@ iframe.preview-frame {
/* GameAgent V1.0 project workbench */
.game-project-workbench {
width: calc(100vw - 72px);
--game-workbench-stage-fill: #fffdfa;
--game-workbench-agent-fill: #fbf7f2;
width: calc(100vw - var(--launcher-sidebar-width));
max-width: none;
min-height: 0;
gap: 10px;
padding: 72px 8px 8px;
gap: 0;
margin: 0;
padding: 0;
overflow: hidden;
color: var(--platform-text-base);
}
@@ -4941,7 +4944,7 @@ iframe.preview-frame {
z-index: 0;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(360px, 0.36fr);
gap: 10px;
gap: 0;
height: clamp(560px, calc(100dvh - 206px), 780px);
min-width: 0;
min-height: 0;
@@ -4951,21 +4954,27 @@ iframe.preview-frame {
.game-workbench-chat {
min-width: 0;
overflow: hidden;
border: 1px solid var(--platform-desktop-panel-border);
border-radius: 16px;
background: var(--platform-desktop-panel-fill);
border: 0;
border-radius: 0;
border-left: 1px solid var(--platform-line-soft);
background: var(--game-workbench-agent-fill);
box-shadow: none;
}
.game-workbench-stage {
position: relative;
display: grid;
grid-template-rows: auto auto minmax(0, 1fr);
box-shadow: var(--platform-panel-shadow);
border-left: 0;
background: var(--game-workbench-stage-fill);
box-shadow: none;
}
.game-workbench-chat {
border-color: var(--platform-line-soft);
box-shadow: var(--platform-nav-active-shadow);
.game-workbench-editor-shell {
border: 0;
border-radius: 0;
background: var(--game-workbench-stage-fill);
box-shadow: none;
}
.game-workbench-stage[data-resource-view-state^='resources.asset-canvas'] {
@@ -4985,8 +4994,7 @@ iframe.preview-frame {
gap: 12px;
min-height: 48px;
padding: 8px 12px;
border-bottom: 1px solid var(--platform-line-soft);
background: var(--platform-desktop-topbar-fill);
background: transparent;
}
.game-workbench-tabs,
@@ -5004,6 +5012,27 @@ iframe.preview-frame {
background: var(--platform-nav-fill);
}
.game-resource-sort-tabs {
flex-wrap: nowrap;
gap: 0;
padding: 0;
overflow: hidden;
}
.game-workbench-tabs.game-resource-sort-tabs button {
min-height: 36px;
border: 0;
border-radius: 0;
}
.game-workbench-tabs.game-resource-sort-tabs button + button {
border-left: 1px solid var(--platform-subpanel-border);
}
.game-workbench-tabs.game-resource-sort-tabs button.is-active {
border-color: transparent;
}
.game-workbench-tabs button,
.game-workbench-view-actions button {
display: inline-flex;
@@ -5030,6 +5059,10 @@ iframe.preview-frame {
box-shadow: 0 0 0 3px var(--platform-input-focus-ring);
}
.game-workbench-tabs.game-resource-sort-tabs button:focus-visible {
box-shadow: inset 0 0 0 3px var(--platform-input-focus-ring);
}
.game-workbench-tabs button.is-active,
.game-workbench-view-actions button.is-active {
border-color: var(--platform-button-primary-border);
@@ -6049,7 +6082,7 @@ iframe.preview-frame {
height: 100%;
min-height: 0;
padding: 12px;
background: #fffdfa;
background: transparent;
}
.game-run-preview {
@@ -6176,12 +6209,12 @@ iframe.preview-frame {
}
.game-workbench-chat > header {
position: relative;
display: flex;
align-items: center;
gap: 9px;
padding: 12px 14px;
border-bottom: 1px solid var(--platform-line-soft);
background: var(--platform-subpanel-fill);
padding: 4px 14px;
background: transparent;
}
.game-workbench-chat > header > div {
@@ -6189,6 +6222,39 @@ iframe.preview-frame {
gap: 2px;
}
.launcher-main:has(.game-project-workbench) > .launcher-account-bar {
display: none;
}
.game-workbench-chat-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
justify-items: center;
text-align: center;
pointer-events: none;
}
.game-workbench-chat-wallet {
position: relative;
z-index: 1;
margin-left: auto;
}
.game-workbench-chat-wallet .launcher-account-bar {
position: static;
}
.game-workbench-editor-actions {
display: flex;
align-items: center;
gap: 8px;
}
.game-workbench-editor-wallet .launcher-account-bar {
position: static;
}
.game-workbench-approval-trigger {
display: inline-flex;
align-items: center;
@@ -6218,15 +6284,11 @@ iframe.preview-frame {
}
.game-workbench-chat-avatar {
display: grid;
width: 30px;
height: 30px;
border-radius: 50%;
background: var(--platform-button-primary-fill);
color: var(--platform-button-primary-text);
font-size: 12px;
font-weight: 900;
place-items: center;
width: 48px;
height: 48px;
flex: 0 0 auto;
object-fit: contain;
filter: drop-shadow(0 5px 9px rgb(182 98 63 / 16%));
}
.game-workbench-chat .project-supervisor-surface {
@@ -6418,8 +6480,7 @@ iframe.preview-frame {
position: relative;
z-index: 2;
padding-top: 8px;
border-top: 1px solid var(--platform-line-soft);
background: var(--platform-desktop-panel-fill);
background: transparent;
}
.game-workbench-chat .project-supervisor-composer textarea {
@@ -6863,8 +6924,8 @@ iframe.preview-frame {
@media (max-width: 760px) {
.game-project-workbench {
width: min(100%, calc(100vw - 76px));
padding-top: 70px;
width: min(100%, calc(100vw - var(--launcher-sidebar-width)));
padding-top: 0;
}
.game-workbench-layout {
@@ -6878,6 +6939,11 @@ iframe.preview-frame {
min-height: 560px;
}
.game-workbench-chat {
border-top: 1px solid var(--platform-line-soft);
border-left: 0;
}
.game-workbench-toolbar,
.game-workbench-view-actions {
align-items: stretch;
@@ -44,6 +44,7 @@ import type {
GameIterationVersion,
ProjectResourceCanvasLayoutMode,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import PRODUCT_LOGO from '../../../../../packages/shared/src/icons/taonier-product-ip.png';
import {
type AssetCanvasCommitNotification,
type AssetCanvasExitResult,
@@ -287,6 +288,7 @@ export type ProjectDevelopmentViewProps = {
agentRuntimeSummaries?: ProjectAgentRuntimeSummary[];
agentResults?: ProjectAgentResultSummary[];
supervisor: ReactNode;
walletEntry?: ReactNode;
onHomeOpen: () => void;
onProjectsOpen: () => void;
onPlay?: () => void;
@@ -726,6 +728,7 @@ export default function ProjectDevelopmentView({
agentRuntimeSummaries = emptyProjectAgentRuntimeSummaries,
agentResults = emptyProjectAgentResults,
supervisor,
walletEntry,
onManifestChange,
onPlay,
}: ProjectDevelopmentViewProps) {
@@ -3107,7 +3110,7 @@ export default function ProjectDevelopmentView({
>
{mode === 'resources' && uiEditorRoute ? (
<div
className="h-full min-h-0 min-w-0 w-full overflow-hidden rounded-2xl border border-(--platform-desktop-panel-border) bg-(--platform-desktop-panel-fill) shadow-(--platform-panel-shadow)"
className="game-workbench-editor-shell h-full min-h-0 min-w-0 w-full overflow-hidden"
data-resource-view-state={resourceViewState}
>
<UiEditorPage
@@ -3115,6 +3118,7 @@ export default function ProjectDevelopmentView({
resourceId={uiEditorRoute.resourceId}
projectId={manifest.projectId}
resourceLabel={uiEditorRoute.resourceLabel}
walletEntry={walletEntry}
onBack={() => setUiEditorRoute(null)}
/>
</div>
@@ -3208,15 +3212,6 @@ export default function ProjectDevelopmentView({
<Sparkles size={15} aria-hidden="true" />
UI
</button>
<button
type="button"
className={sortMode === 'dependency' ? 'is-active' : ''}
aria-pressed={sortMode === 'dependency'}
onClick={() => showResourceSortMode('dependency')}
>
<FolderTree size={15} aria-hidden="true" />
</button>
<span
className="game-resource-reorder-status"
role="status"
@@ -3224,15 +3219,32 @@ export default function ProjectDevelopmentView({
>
{resourceLayoutSaving ? '保存中' : resourceLayoutNotice}
</span>
<button
type="button"
className={sortMode === 'type' ? 'is-active' : ''}
aria-pressed={sortMode === 'type'}
onClick={() => showResourceSortMode('type')}
<div
className="game-workbench-tabs game-resource-sort-tabs"
role="group"
aria-label="资源排列方式"
>
<ListFilter size={15} aria-hidden="true" />
</button>
<button
type="button"
className={sortMode === 'dependency' ? 'is-active' : ''}
aria-label="按依赖"
aria-pressed={sortMode === 'dependency'}
onClick={() => showResourceSortMode('dependency')}
>
<FolderTree size={15} aria-hidden="true" />
</button>
<button
type="button"
className={sortMode === 'type' ? 'is-active' : ''}
aria-label="按类型"
aria-pressed={sortMode === 'type'}
onClick={() => showResourceSortMode('type')}
>
<ListFilter size={15} aria-hidden="true" />
</button>
</div>
</>
) : null}
</div>
@@ -4015,13 +4027,20 @@ export default function ProjectDevelopmentView({
<aside className="game-workbench-chat" aria-label="陶泥儿 Agent 对话">
<header>
<span className="game-workbench-chat-avatar"></span>
<div>
<img
className="game-workbench-chat-avatar"
src={PRODUCT_LOGO}
alt="陶泥儿"
/>
<div className="game-workbench-chat-title">
<strong></strong>
<small>
{professionalDagVisible ? '项目总控 Agent' : '智能创作'}
</small>
</div>
{walletEntry ? (
<div className="game-workbench-chat-wallet">{walletEntry}</div>
) : null}
{professionalDagVisible ? (
<button
type="button"
@@ -1,5 +1,5 @@
import { ChevronLeft } from 'lucide-react';
import { useMemo, useState } from 'react';
import { type ReactNode, useMemo, useState } from 'react';
import { ThemedModal } from '../../components/modal/ThemedModal';
import {
@@ -27,6 +27,7 @@ export default function UiEditorPage({
resourceId,
projectId,
resourceLabel,
walletEntry,
onBack,
stateStore,
}: {
@@ -34,6 +35,7 @@ export default function UiEditorPage({
resourceId?: string;
projectId?: string;
resourceLabel?: string;
walletEntry?: ReactNode;
onBack?: () => void;
stateStore?: IUiDesignStateStore;
}) {
@@ -92,20 +94,25 @@ export default function UiEditorPage({
</button>
<strong className="text-sm">{resourceLabel ?? 'UI 设计'}</strong>
<button
type="button"
className="rounded-lg bg-orange-600 px-3 py-1.5 text-sm font-semibold text-white disabled:opacity-60"
disabled={
session.save.isSaving ||
session.save.isLoading ||
Boolean(session.save.loadError) ||
session.save.persistedRevision === null ||
session.save.isLocked
}
onClick={() => requestSave()}
>
{session.save.isSaving ? '保存中…' : '保存'}
</button>
<div className="game-workbench-editor-actions">
{walletEntry ? (
<div className="game-workbench-editor-wallet">{walletEntry}</div>
) : null}
<button
type="button"
className="rounded-lg bg-orange-600 px-3 py-1.5 text-sm font-semibold text-white disabled:opacity-60"
disabled={
session.save.isSaving ||
session.save.isLoading ||
Boolean(session.save.loadError) ||
session.save.persistedRevision === null ||
session.save.isLocked
}
onClick={() => requestSave()}
>
{session.save.isSaving ? '保存中…' : '保存'}
</button>
</div>
</header>
) : null}
{session.save.saveError ? (
@@ -3429,7 +3429,13 @@ export function registerProjectWorkbenchFoundationTests() {
);
expect(styles).toMatch(
/\.game-project-workbench\s*\{[^}]*width:\s*calc\(100vw - 72px\)[^}]*padding:\s*72px 8px 8px[^}]*overflow:\s*hidden/s,
/\.game-project-workbench\s*\{[^}]*width:\s*calc\(100vw - var\(--launcher-sidebar-width\)\)[^}]*padding:\s*0[^}]*overflow:\s*hidden/s,
);
expect(styles).toMatch(
/\.launcher-main:has\(\.game-project-workbench\)\s*\{[^}]*padding-bottom:\s*0/s,
);
expect(styles).toMatch(
/\.game-workbench-toolbar\s*\{[^}]*padding:\s*8px 12px[^}]*background:\s*transparent/s,
);
expect(styles).toMatch(
/@media \(min-width: 761px\)[\s\S]*?\.game-project-workbench\s*\{[^}]*grid-template-rows:\s*minmax\(0, 1fr\) auto[^}]*height:\s*100dvh/,
@@ -3444,8 +3450,12 @@ export function registerProjectWorkbenchFoundationTests() {
/\.game-workbench-chat \.project-runtime-summary\s*\{[^}]*position:\s*sticky[^}]*top:\s*-10px/s,
);
expect(styles).toMatch(
/\.game-workbench-chat \.project-supervisor-composer\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\) 82px[^}]*z-index:\s*2[^}]*border-top:\s*1px solid var\(--platform-line-soft\)/s,
/\.game-workbench-chat \.project-supervisor-composer\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\) 82px[^}]*z-index:\s*2[^}]*padding-top:\s*8px[^}]*background:\s*transparent/s,
);
const composerRule = styles.match(
/\.game-workbench-chat \.project-supervisor-composer\s*\{([^}]*)\}/s,
);
expect(composerRule?.[1]).not.toContain('border-top:');
expect(styles).toMatch(
/\.game-workbench-chat \.project-supervisor-composer button\s*\{[^}]*min-height:\s*72px[^}]*white-space:\s*nowrap/s,
);
@@ -3482,6 +3492,41 @@ export function registerProjectWorkbenchFoundationTests() {
);
});
it('keeps the wallet entry available when the workbench opens the UI editor', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
const projectDevelopmentSource = readFileSync(
resolve(
process.cwd(),
'apps/ai-game-creator-shell/src/view/project-development/index.tsx',
),
'utf8',
);
expect(styles).toMatch(
/\.launcher-main:has\(\.game-project-workbench\)\s*>\s*\.launcher-account-bar\s*\{[^}]*display:\s*none/s,
);
expect(projectDevelopmentSource).toMatch(
/<UiEditorPage[\s\S]*?walletEntry=\{walletEntry\}[\s\S]*?\/>/,
);
expect(projectDevelopmentSource).toMatch(
/<div className="game-workbench-chat-wallet">\{walletEntry\}<\/div>/,
);
});
it('keeps resource sort tab keyboard focus inside the clipped segmented control', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
'utf8',
);
expect(styles).toMatch(
/\.game-workbench-tabs\.game-resource-sort-tabs button:focus-visible\s*\{[^}]*box-shadow:\s*inset 0 0 0 3px var\(--platform-input-focus-ring\)/s,
);
});
it('keeps workbench chat bubbles aligned without shrinking process cards', () => {
const styles = readFileSync(
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
@@ -9200,7 +9245,8 @@ export function registerProjectSupervisorSurfaceTests() {
projectPath,
});
let directTurnUpdateHandler:
((event: { payload: Record<string, unknown> }) => void) | null = null;
| ((event: { payload: Record<string, unknown> }) => void)
| null = null;
const listen = vi.fn(
async (
eventName: string,
@@ -10157,7 +10203,8 @@ export function registerProjectAgentStatusTests() {
},
);
let runtimeUpdateHandler:
((event: { payload: Record<string, unknown> }) => void) | null = null;
| ((event: { payload: Record<string, unknown> }) => void)
| null = null;
const listen = vi.fn(
async (
eventName: string,
@@ -146,6 +146,34 @@ async function renderLoadedSession(state: State) {
}
describe('UiEditorPage', () => {
it('keeps the wallet entry in the resource editor header', async () => {
const stateStore: IUiDesignStateStore = {
load: vi.fn().mockResolvedValue(structuredClone(EMPTY_SNAPSHOT)),
save: vi.fn(),
};
render(
createElement(UiEditorPage, {
projectPath: '/tmp/ui-editor-wallet',
resourceId: 'ui-resource',
stateStore,
walletEntry: createElement(
'button',
{ type: 'button', 'aria-label': '打开账户资产' },
'泥点',
),
}),
);
const walletEntry = await screen.findByRole('button', {
name: '打开账户资产',
});
expect(walletEntry.closest('header')).not.toBeNull();
expect(
screen.getAllByRole('button', { name: '打开账户资产' }),
).toHaveLength(1);
});
it('cancels image preview scopes when the resource changes and on unmount', async () => {
vi.mocked(invoke)
.mockReset()
@@ -108,6 +108,9 @@
- 第一批共享 chrome 固定覆盖画布动作按钮、工具栏、工具分组和分隔符。按钮的默认、悬停、键盘焦点、选中、禁用和主次色语义由共享层表达;宿主只提供图标、文案、事件与业务禁用条件。
- 主站账号、钱包、服务端项目、云端素材库和生成面板仍留在网站宿主;客户端本地项目、manifest、Runtime、审批、草稿、生成与正式提交仍留在 Tauri 宿主。共享视觉组件不得读取这些业务事实。
- 客户端 UI 对齐采用“同视觉、同组件、保留四区布局”,不复制主站整页布局。资源总览、运行视图和 Supervisor 对话继续是 Game Agent 工作台独有语义。
- 工作台四区作为同一应用工作面直接衔接,外层不使用卡片圆角或留白间距。中央主视窗与右侧 Supervisor 使用各自的平台区域底色,并以稳定的竖向分隔线明确边界;不通过外层圆角容器、阴影或区块间缝重新包装四区。
- 右侧正式钱包入口属于客户端宿主 chrome,在普通项目工作台与 UI Editor 子路由中都必须持续可见、键盘可达且可操作,不得因进入素材编辑壳或隐藏全局 chrome 而丢失余额、充值和使用详情入口。
- 资源总览的“资源依赖 / 资源类型”视图切换使用连通的分段按钮组,相邻选项共享边界并保持唯一选中语义。每个分段都必须有清晰的键盘焦点指示,焦点环不得被分段容器的圆角或 `overflow` 裁切。
- 右侧 Supervisor 对话中,用户消息使用右对齐、最大宽度受限的主题暖色气泡,assistant 消息保持左对齐;消息换行不得产生水平溢出,执行过程卡继续占满消息区可用宽度。气泡正文在 light / dark 平台主题下均须满足 WCAG AA 普通文本 `4.5:1` 对比度。
- 客户端正式产品仍只按最小 `1280×720` 横屏合同交付,并保留 `1280×800` 默认窗口与既有基线验收;更窄浏览器样式只负责不崩溃和开发兼容,不改成移动端创作工作台。
- 普通用户界面不默认展示内部错误码或本机绝对路径,也不展示 External Editor Base URL 或 Developer API Key;官方服务地址由客户端构建固定,远端编辑自动使用当前陶泥儿登录态。仅独立 standalone game-chat release 或显式高级自定义模式在隔离配置页维护 External v1 URL/Key,确需诊断的信息进入受控详情或开发模式。
@@ -567,6 +570,9 @@ type ProjectAgentMudPointAttribution = {
4. 主站现有图片编辑器的按钮名称、tool selection、hover/focus/pressed/disabled 行为与工具栏位置不因共享抽取回归;网站账号、钱包、素材库、生成和导出逻辑不下沉共享层。
5. 客户端生成、保存、取消、恢复、登录失效与余额不足只更换视觉承载,不改变 Host Port 调用、幂等身份、草稿 CAS、manifest/revision、Runtime 审批或钱包刷新语义。
6. 阶段一、二冻结合同并让主站消费共享 chrome;2026-08-06 的阶段三至五已经完成客户端中央区、Supervisor 和 Agent Dock 的正式换肤、定向测试与 `1280×800` 验收。独立 Vite 没有登录会话时继续显示真实登录门禁,不为视觉测试增加绕过入口。
7. 普通工作台和 UI Editor 子路由均不显示工作面外层圆角或区块留白;中央区与右侧 Supervisor 的区域底色可辨识,竖向分隔连续、无错位。
8. 右侧正式钱包入口在普通工作台和 UI Editor 子路由都始终可见、键盘可达并能打开余额、充值与使用详情;路由切换不得使入口消失或失去交互。
9. “资源依赖 / 资源类型”以连通分段按钮呈现,点击与键盘操作均只保留一个选中项;使用 Tab 定位和键盘切换时焦点指示清晰、完整,不被容器边界或 `overflow` 裁切。
## 8. 非目标