diff --git a/apps/ai-game-creator-shell/src-tauri/capabilities/window-chrome.json b/apps/ai-game-creator-shell/src-tauri/capabilities/window-chrome.json new file mode 100644 index 000000000..a23607f6f --- /dev/null +++ b/apps/ai-game-creator-shell/src-tauri/capabilities/window-chrome.json @@ -0,0 +1,13 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "window-chrome", + "description": "自绘标题栏允许执行当前窗口的基础控制和拖拽。", + "windows": ["client", "developer", "main", "launcher", "supervisor-chat"], + "permissions": [ + "core:window:allow-close", + "core:window:allow-is-maximized", + "core:window:allow-minimize", + "core:window:allow-start-dragging", + "core:window:allow-toggle-maximize" + ] +} diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-client-projection/SKILL.md b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-client-projection/SKILL.md index 93e2fa35a..2ad081566 100644 --- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-client-projection/SKILL.md +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-client-projection/SKILL.md @@ -10,7 +10,7 @@ Let the client derive projections from real disk changes and trusted tool result ## Workflow 1. Write executable source to `index.html`, `style.css`, and `game.js` in the current cwd. Use only relative paths returned by approved tools for media. -2. Before using or deriving an existing registered asset, call `agc_list_registered_assets` and select its `localAssetId`. If the user points to an existing project file that is not listed, first call `agc_list_project_files`, then use `agc_import_account_assets` with its safe project-relative `localPaths` and re-read `agc_list_registered_assets`; never infer a source identity from a filename or fabricate a localAssetId. +2. Before using or deriving an existing registered asset, call `agc_list_registered_assets` and select its `localAssetId`. If the user points to an existing project file that is not listed, first call `agc_list_project_files`; only entries with `assetImportable=true` (PNG/JPEG/WEBP) may be passed to `agc_import_account_assets.localPaths`. Then re-read `agc_list_registered_assets`; never infer a source identity from a filename or fabricate a localAssetId. 3. Keep read scopes separate: `asset.list` is the current project manifest, `asset.library.list` is the signed-in account library, and the web project's canvas resource read model is the authoritative canvas list. The account library is not the complete canvas list. 4. Use `canvas.asset_import` for safe account/canvas asset IDs or project-relative local paths. The client rechecks ownership and validates bytes; host absolute paths require native UI file-picker authorization. 5. When the user explicitly asks to create or derive video, character animation, sound effect, or background music, call `agc_create_or_derive_resource`. Use `create` only for video/audio without a source and `derive` with a registered `sourceLocalAssetId`; character animation is always derived from an image. diff --git a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-client-projection/references/projection-contract.md b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-client-projection/references/projection-contract.md index eba137dad..8bce1c850 100644 --- a/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-client-projection/references/projection-contract.md +++ b/apps/ai-game-creator-shell/src-tauri/resources/agc-skills/agc-client-projection/references/projection-contract.md @@ -8,7 +8,7 @@ The client projects three distinct facts: Do not collapse these facts. A playable file can exist before projection refresh, a registered image can exist without being used by the game, and browser success does not create platform provenance. -`agc_list_project_files` is the bounded Direct discovery path for real project files. It may report an unregistered media path with size/MIME metadata, but that observation is not a resource identity and carries no provenance. `agc_import_account_assets.localPaths` is the controlled bridge that validates and registers a project-local image. `agc_list_registered_assets` remains the authoritative Direct read path for manifest resource identity; only its stable identifiers may be passed to generation/derivation tools. +`agc_list_project_files` is the bounded Direct discovery path for real project files. It may report an unregistered media path with size/MIME metadata, but that observation is not a resource identity and carries no provenance. Its `assetImportable` field is true only for PNG/JPEG/WEBP files accepted by the current local-image registration contract; GIF/SVG and non-image files remain discoverable but must not be passed to the image importer. `agc_import_account_assets.localPaths` is the controlled bridge that validates and registers an importable project-local image. `agc_list_registered_assets` remains the authoritative Direct read path for manifest resource identity; only its stable identifiers may be passed to generation/derivation tools. Read scopes remain separate: `asset.list` is the current project's local manifest, `asset.library.list` is the signed-in account library, and the web project's canvas resource read model is authoritative for resources visible on that canvas. A library result must not be presented as the complete canvas list. `canvas.asset_import` accepts safe account/canvas asset IDs or project-relative local paths; receipts expose only bounded counts, safe IDs, relative paths, sources, redacted failures, and `revisionAdvanceCount`. diff --git a/apps/ai-game-creator-shell/src-tauri/src/windows.rs b/apps/ai-game-creator-shell/src-tauri/src/windows.rs index e6321414a..fc146f387 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/windows.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/windows.rs @@ -165,6 +165,7 @@ pub(crate) fn open_game_creator_workspace_window( } tauri::WebviewWindowBuilder::new(&app, "main", workspace_window_url(project_path)) .title("陶泥儿") + .decorations(false) .inner_size(1180.0, 820.0) .min_inner_size(760.0, 560.0) .build() @@ -183,6 +184,7 @@ pub(crate) fn open_game_creator_launcher_window( } else { tauri::WebviewWindowBuilder::new(&app, "launcher", launcher_window_url()) .title("陶泥儿") + .decorations(false) .inner_size(820.0, 640.0) .min_inner_size(720.0, 520.0) .build() @@ -232,6 +234,7 @@ pub(crate) fn open_project_supervisor_chat_window( supervisor_chat_window_url(project_path), ) .title("项目总控 Agent 对话") + .decorations(false) .inner_size(820.0, 720.0) .min_inner_size(560.0, 480.0) .build() diff --git a/apps/ai-game-creator-shell/src-tauri/tauri.conf.json b/apps/ai-game-creator-shell/src-tauri/tauri.conf.json index 2f6356dc5..e079a26b0 100644 --- a/apps/ai-game-creator-shell/src-tauri/tauri.conf.json +++ b/apps/ai-game-creator-shell/src-tauri/tauri.conf.json @@ -18,6 +18,7 @@ "url": "index.html", "width": 1280, "height": 800, + "decorations": false, "minWidth": 1280, "minHeight": 720 } diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index c0e70c4d1..c46441dfc 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -93,6 +93,7 @@ import type { TauriInvoke, UploadLocalAssetResult, } from './app/types'; +import { useWindowChrome } from './components/windowChromeContext'; import { agentConversationId, agentRuntimeCancelStatus, @@ -437,6 +438,7 @@ export function App({ onAgentRuntimeSummariesChange, onAgentResultsChange, }: AppProps = {}) { + const { setTitle: setWindowTitle } = useWindowChrome(); // 做方案入口独立成链:立项策划需要委派、澄清 pending 与 GDD 审批,这些只存在于 // Supervisor Runtime;direct-codex 是单回合「生成→试玩→修」循环,没有对应机制。 // 因此策划入口不走产品默认的 direct-codex,做游戏与做素材保持 master 的新默认。 @@ -10602,6 +10604,13 @@ export function App({ const currentProjectTitle = localProject ? manifest.name.trim() || projectNameFromPath(localProject.projectPath) : seedManifest.name; + useEffect(() => { + if (!supervisorChatOnly) { + return; + } + setWindowTitle(localProject ? currentProjectTitle : undefined); + return () => setWindowTitle(undefined); + }, [currentProjectTitle, localProject, setWindowTitle, supervisorChatOnly]); const agentRunHistoryOmittedCount = Math.max(0, agentRunHistoryFiles.length - agentRunHistoryVisibleCount) + agentRunHistoryOverflowCount; diff --git a/apps/ai-game-creator-shell/src/components/WindowChrome.tsx b/apps/ai-game-creator-shell/src/components/WindowChrome.tsx new file mode 100644 index 000000000..77c69f1a0 --- /dev/null +++ b/apps/ai-game-creator-shell/src/components/WindowChrome.tsx @@ -0,0 +1,198 @@ +import { getCurrentWindow } from '@tauri-apps/api/window'; +import { Copy, Minus, Square, X } from 'lucide-react'; +import { type ReactNode, useCallback, useEffect, useState } from 'react'; + +import brandIcon from '../../../../packages/shared/src/icons/taonier-product-ip.png'; +import { + WINDOW_CHROME_DEFAULT_TITLE, + WindowChromeContext, + type WindowChromeContextValue, +} from './windowChromeContext'; + +type WindowChromeProps = { + children: ReactNode; +}; + +function isTauriRuntime() { + return ( + typeof window !== 'undefined' && + Boolean( + (window as Window & { __TAURI_INTERNALS__?: unknown }) + .__TAURI_INTERNALS__, + ) + ); +} + +function getNativeWindow() { + if (!isTauriRuntime()) { + return null; + } + try { + return getCurrentWindow(); + } catch { + return null; + } +} + +export function WindowChrome({ children }: WindowChromeProps) { + const [title, setTitleState] = useState(WINDOW_CHROME_DEFAULT_TITLE); + const [walletSlot, setWalletSlot] = useState(null); + + const setTitle = useCallback((nextTitle: string | null | undefined) => { + const normalizedTitle = nextTitle?.trim(); + setTitleState(normalizedTitle || WINDOW_CHROME_DEFAULT_TITLE); + }, []); + + const contextValue: WindowChromeContextValue = { + isWindowChrome: true, + title, + setTitle, + walletSlot, + }; + + const [isMaximized, setIsMaximized] = useState(false); + + useEffect(() => { + const nativeWindow = getNativeWindow(); + if (!nativeWindow) { + return; + } + + let disposed = false; + let unlistenResize: (() => void) | undefined; + + const syncMaximizedState = () => { + void nativeWindow + .isMaximized() + .then((maximized) => { + if (!disposed) { + setIsMaximized(maximized); + } + }) + .catch(() => undefined); + }; + + syncMaximizedState(); + void nativeWindow + .onResized(syncMaximizedState) + .then((unlisten) => { + if (disposed) { + unlisten(); + } else { + unlistenResize = unlisten; + } + }) + .catch(() => undefined); + + return () => { + disposed = true; + unlistenResize?.(); + }; + }, []); + + async function minimizeWindow() { + const nativeWindow = getNativeWindow(); + if (!nativeWindow) { + return; + } + await nativeWindow.minimize().catch(() => undefined); + } + + async function toggleMaximizeWindow() { + const nativeWindow = getNativeWindow(); + if (!nativeWindow) { + return; + } + await nativeWindow + .toggleMaximize() + .then(() => nativeWindow.isMaximized()) + .then(setIsMaximized) + .catch(() => undefined); + } + + async function closeWindow() { + const nativeWindow = getNativeWindow(); + if (!nativeWindow) { + return; + } + await nativeWindow.close().catch(() => undefined); + } + + return ( + +
+
+
+
+ + + + + 陶泥儿 + GameAgent + +
+
+ +
+ + +
+ +
+
+
+ + + +
+
+
+
{children}
+
+
+ ); +} diff --git a/apps/ai-game-creator-shell/src/components/windowChromeContext.ts b/apps/ai-game-creator-shell/src/components/windowChromeContext.ts new file mode 100644 index 000000000..e84a79830 --- /dev/null +++ b/apps/ai-game-creator-shell/src/components/windowChromeContext.ts @@ -0,0 +1,21 @@ +import { createContext, useContext } from 'react'; + +export const WINDOW_CHROME_DEFAULT_TITLE = '创作工作台'; + +export type WindowChromeContextValue = { + isWindowChrome: boolean; + title: string; + setTitle: (title: string | null | undefined) => void; + walletSlot: HTMLElement | null; +}; + +export const WindowChromeContext = createContext({ + isWindowChrome: false, + title: WINDOW_CHROME_DEFAULT_TITLE, + setTitle: () => undefined, + walletSlot: null, +}); + +export function useWindowChrome() { + return useContext(WindowChromeContext); +} diff --git a/apps/ai-game-creator-shell/src/features/app-shell/WorkspaceLauncher.tsx b/apps/ai-game-creator-shell/src/features/app-shell/WorkspaceLauncher.tsx index 10e4fd343..6d63481a4 100644 --- a/apps/ai-game-creator-shell/src/features/app-shell/WorkspaceLauncher.tsx +++ b/apps/ai-game-creator-shell/src/features/app-shell/WorkspaceLauncher.tsx @@ -1,9 +1,14 @@ import { Fragment, useCallback, useEffect, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import { launcherNotifications } from '../../app/constants'; import { closeDialogOnEscape } from '../../app/dialogs'; import { resolveTauriInvoke } from '../../app/tauri'; import type { LocalGameProjectRevisionStatus } from '../../app/types'; +import { + useWindowChrome, + WINDOW_CHROME_DEFAULT_TITLE, +} from '../../components/windowChromeContext'; import HomeView from '../../view/home'; import { type LauncherView, Sidebar } from '../../view/layout'; import ProjectDevelopmentView from '../../view/project-development'; @@ -33,6 +38,11 @@ export function WorkspaceLauncherShell({ initialView = 'home', ProjectSupervisor, }: WorkspaceLauncherShellProps) { + const { + isWindowChrome, + setTitle: setWindowTitle, + walletSlot, + } = useWindowChrome(); const accountWallet = useAccountWallet(currentUser.id); const [status, setStatus] = useState(''); const recentProjects = useRecentProjects(setStatus); @@ -74,6 +84,15 @@ export function WorkspaceLauncherShell({ const manifestMergeRef = useRef(null); activeProjectContextRef.current = currentProjectContext; + useEffect(() => { + const nextTitle = + launcherView === 'project-development' + ? currentProjectContext?.projectName.trim() + : ''; + setWindowTitle(nextTitle || WINDOW_CHROME_DEFAULT_TITLE); + return () => setWindowTitle(WINDOW_CHROME_DEFAULT_TITLE); + }, [currentProjectContext?.projectName, launcherView, setWindowTitle]); + useEffect(() => { const current = activeProjectContextRef.current; manifestMergeRef.current = @@ -268,8 +287,6 @@ export function WorkspaceLauncherShell({ ) : null} - - {launcherView === 'home' ? ( 0} @@ -307,7 +324,6 @@ export function WorkspaceLauncherShell({ agentRuntimeSummaries={activeProjectAgentRuntimeSummaries} agentResults={activeProjectAgentResults} planningStartMode={currentProjectContext.startMode === 'planning'} - walletEntry={} onPlay={() => requestCurrentProjectPlay(currentProjectContext.projectPath) } @@ -365,6 +381,14 @@ export function WorkspaceLauncherShell({ /> ) : null} + {walletSlot ? ( + createPortal( + , + walletSlot, + ) + ) : isWindowChrome ? null : ( + + )} {launcherNotice ? (
- - {({ user, logout }) => - supervisorChatMode ? ( - - ) : ( - - ) - } - + + + {({ user, logout }) => + supervisorChatMode ? ( + + ) : ( + + ) + } + + , ); diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index 2f8f6e3e2..ab77aaed5 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -16,6 +16,346 @@ body { margin: 0; } +#root { + height: 100dvh; + min-height: 0; +} + +/* 桌面壳统一承载标题栏,让 Tauri 与浏览器预览共用同一表面。 */ +.window-chrome { + --window-chrome-height: 50px; + --window-chrome-title-side-space: 300px; + display: grid; + grid-template-rows: var(--window-chrome-height) minmax(0, 1fr); + width: 100%; + height: 100dvh; + min-height: 0; + overflow: hidden; + background: var(--platform-body-fill, #fffdf9); + color: var(--platform-text-strong, #3d1f10); +} + +.window-chrome__bar { + position: relative; + z-index: 30; + display: grid; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + align-items: center; + min-width: 0; + height: var(--window-chrome-height); + padding: 0; + overflow: visible; + border-bottom: 1px solid var(--platform-surface-border, #ead8cb); + background: radial-gradient( + circle at 16% -80%, + rgb(255 255 255 / 88%), + transparent 38% + ), + radial-gradient(circle at 72% 0%, rgb(237 190 151 / 20%), transparent 28%), + var( + --platform-desktop-topbar-fill, + linear-gradient(180deg, #fffefa, #fbf1e9) + ); + box-shadow: 0 5px 18px rgb(112 57 30 / 7%); + user-select: none; + -webkit-app-region: drag; +} + +.window-chrome__bar::after { + position: absolute; + right: 0; + bottom: -1px; + left: 0; + height: 1px; + background: linear-gradient( + 90deg, + transparent, + rgb(199 101 61 / 22%), + transparent + ); + content: ''; + pointer-events: none; +} + +.window-chrome__leading, +.window-chrome__trailing, +.window-chrome__controls, +.window-chrome__brand, +.window-chrome__wallet-slot { + -webkit-app-region: no-drag; +} + +.window-chrome__leading, +.window-chrome__trailing { + display: flex; + align-items: center; + min-width: 0; +} + +.window-chrome__leading { + grid-column: 1; + position: relative; + z-index: 1; + justify-self: start; + gap: 12px; + padding-left: 12px; +} + +.window-chrome__brand { + display: inline-flex; + align-items: center; + flex: 0 0 auto; + gap: 8px; + min-width: 132px; + color: var(--platform-text-strong, #3d1f10); +} + +.window-chrome__brand-mark { + display: grid; + width: 32px; + height: 32px; + place-items: center; + border: 1px solid rgb(199 101 61 / 20%); + border-radius: 11px; + background: linear-gradient(145deg, #fffaf2 5%, #f4d8c3 100%); + box-shadow: + 0 5px 12px rgb(182 98 63 / 14%), + inset 0 1px 0 rgb(255 255 255 / 90%); +} + +.window-chrome__brand-mark img { + width: 27px; + height: 27px; + object-fit: contain; + filter: drop-shadow(0 3px 4px rgb(139 69 39 / 14%)); +} + +.window-chrome__brand-copy { + display: grid; + gap: 1px; + line-height: 1; +} + +.window-chrome__brand-copy strong { + color: var(--platform-brand-logo-title, #4a220f); + font-size: 14px; + font-weight: 800; + letter-spacing: 0.02em; +} + +.window-chrome__brand-copy span { + color: var(--platform-brand-logo-subtitle, #c7653d); + font-size: 9px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.window-chrome__drag-region { + position: absolute; + z-index: 0; + inset: 0; + display: grid; + place-items: center; + width: auto; + min-width: 0; + color: var(--platform-text-muted, #a38f80); + font-size: 11px; + font-weight: 650; + letter-spacing: 0.04em; + cursor: default; + padding-inline: var(--window-chrome-title-side-space); + -webkit-app-region: drag; +} + +.window-chrome__title-wrap { + position: relative; + display: block; + min-width: 0; + max-width: min(42vw, 460px, 100%); +} + +.window-chrome__title { + display: block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.window-chrome__workspace-dot { + position: absolute; + top: 50%; + right: calc(100% + 9px); + width: 6px; + height: 6px; + margin: 0; + border-radius: 50%; + background: var(--platform-accent, #c7653d); + box-shadow: 0 0 0 4px rgb(199 101 61 / 10%); + transform: translateY(-50%); +} + +.window-chrome__trailing { + grid-column: 3; + position: relative; + z-index: 1; + justify-self: end; + flex: 0 0 auto; + gap: 12px; + padding-left: 8px; +} + +.window-chrome__wallet-slot { + display: flex; + align-items: center; + min-width: 0; +} + +.window-chrome__wallet-slot > .launcher-account-bar { + position: static; + top: auto; + right: auto; +} + +.window-chrome__controls { + display: flex; + align-self: stretch; +} + +.window-chrome__control { + display: grid; + width: 44px; + height: var(--window-chrome-height); + place-items: center; + border: 0; + border-radius: 0; + background: transparent; + color: var(--platform-text-base, #6f5848); + cursor: pointer; + transition: + background 150ms ease, + color 150ms ease; +} + +.window-chrome__control:hover, +.window-chrome__control:focus-visible { + background: rgb(204 117 76 / 12%); + color: var(--platform-text-strong, #3d1f10); + outline: none; +} + +.window-chrome__control--close:hover, +.window-chrome__control--close:focus-visible { + background: #c7653d; + color: #fffaf5; +} + +.window-chrome__content { + position: relative; + z-index: 0; + height: 100%; + min-width: 0; + min-height: 0; + overflow: hidden; +} + +.window-chrome__content > .client-auth-shell, +.window-chrome__content > .launcher-shell, +.window-chrome__content > .app-shell { + height: 100%; + min-height: 0; +} + +.window-chrome__content > .launcher-shell > aside { + height: 100%; + min-height: 0; +} + +.window-chrome__content > .launcher-shell .launcher-main, +.window-chrome__content > .app-shell.app-shell--dev { + min-height: 0; +} + +.window-chrome__content > .app-shell.app-shell--user .chat-pane { + min-height: calc(100% - 48px); +} + +.window-chrome__content .launcher-projects-page, +.window-chrome__content .launcher-main:has(.launcher-projects-page) { + height: 100%; +} + +.window-chrome__content > .launcher-shell .launcher-main { + height: 100%; + min-height: 0; +} + +.window-chrome__content + > .launcher-shell + .launcher-main + > .platform-theme.min-h-screen { + height: 100%; + min-height: 0; + overflow: auto; +} + +.window-chrome__content .launcher-main:has(.game-project-workbench) { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + padding-bottom: 0; +} + +.window-chrome__content .game-project-workbench { + flex: 1 1 auto; + height: auto; + min-height: 0; +} + +.window-chrome__content .game-workbench-layout { + height: 100%; + min-height: 0; +} + +@media (max-width: 760px) { + .window-chrome { + --window-chrome-height: 46px; + } + + .window-chrome__bar { + padding: 0; + } + + .window-chrome__brand { + min-width: 38px; + } + + .window-chrome__brand-copy, + .window-chrome__title { + display: none; + } + + .window-chrome__drag-region { + display: none; + padding-inline: 0; + } + + .window-chrome__leading { + gap: 5px; + padding-left: 8px; + } + + .window-chrome__trailing { + padding-left: 2px; + } + + .window-chrome__control { + width: 40px; + } +} + button, input, select, diff --git a/apps/ai-game-creator-shell/tests/WindowChrome.test.tsx b/apps/ai-game-creator-shell/tests/WindowChrome.test.tsx new file mode 100644 index 000000000..02f8fec2a --- /dev/null +++ b/apps/ai-game-creator-shell/tests/WindowChrome.test.tsx @@ -0,0 +1,85 @@ +// @vitest-environment jsdom + +import { fireEvent, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, expect, it } from 'vitest'; + +import { WindowChrome } from '../src/components/WindowChrome'; +import { useWindowChrome } from '../src/components/windowChromeContext'; + +function TitleSetter({ value }: { value: string }) { + const { setTitle } = useWindowChrome(); + return ( + + ); +} + +describe('WindowChrome', () => { + it('renders the陶泥儿 brand, default title, and controls', async () => { + const user = userEvent.setup(); + render( + +
工作区内容
+
, + ); + + expect(screen.getByRole('banner', { name: '窗口标题栏' })).toBeTruthy(); + expect(screen.getByLabelText('陶泥儿 GameAgent')).toBeTruthy(); + expect(screen.queryByLabelText('本地工作区')).toBeNull(); + expect(screen.getByText('创作工作台')).toBeTruthy(); + expect(screen.getByText('工作区内容')).toBeTruthy(); + expect(screen.queryByRole('menubar')).toBeNull(); + + await user.click(screen.getByRole('button', { name: '最小化' })); + await user.click(screen.getByRole('button', { name: '最大化' })); + await user.click(screen.getByRole('button', { name: '关闭' })); + }); + + it('updates the centered title through the shell context', async () => { + const user = userEvent.setup(); + render( + + + , + ); + + await user.click(screen.getByRole('button', { name: '更新标题' })); + + expect(screen.getByText('星尘花园')).toBeTruthy(); + expect(screen.getByLabelText('当前工作区:星尘花园')).toBeTruthy(); + }); + + it('restores the default title when a page clears its title', async () => { + const user = userEvent.setup(); + function ClearTitle() { + const { setTitle } = useWindowChrome(); + return ( + + ); + } + render( + + + , + ); + + await user.click(screen.getByRole('button', { name: '清空标题' })); + + expect(screen.getByText('创作工作台')).toBeTruthy(); + }); + + it('closes no menus because the title bar has no application menu bar', () => { + render( + + + , + ); + + fireEvent.pointerDown(screen.getByRole('button', { name: '页面按钮' })); + expect(screen.queryByRole('menu')).toBeNull(); + }); +}); diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 4f5edc6b1..43bdbe7d2 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -142,6 +142,7 @@ Supervisor 认领该回执后,由父 run 自己为每个原 delivery 逐一创 ## 技术选择 - 桌面壳:新建 `apps/ai-game-creator-shell`,与现有 `apps/desktop-shell` 分离,避免把游戏创作本地能力塞进主站宿主壳;启动时先检查平台登录态,未登录只展示登录页,登录后进入单窗口客户端首页;正式用户窗口常驻左侧栏和顶部栏,并在首页、项目组、指南 / 反馈和项目开发页之间切换。发布和 debug 启动都只登记并打开 `client` 用户窗口;`index.html?agent-chat` 仅保留为显式前端调试路由,不是 Tauri 自动启动入口。 +- 窗口外壳:`client`、动态 `main` / `launcher` / `supervisor-chat` 窗口统一关闭原生 decorations,由前端 `WindowChrome` 绘制陶泥儿品牌 Logo、当前页面 / 项目标题和最小化 / 最大化 / 关闭控制。标题栏不重复展示项目列表或本地工作区入口:首页及非项目页面的居中标题固定为“创作工作台”,打开项目后切换为当前项目名;泥点账户入口通过标题栏右侧插槽渲染,标题文本使用独立的窗口几何居中层,不参与左右入口宽度分配,项目名过长时仅在可用宽度内省略。标题栏只复用 `packages/shared/src/theme.css` 的暖陶土变量与现有产品 IP,不引入另一套主题;浏览器预览或非 Tauri 宿主中窗口控制安全降级,不能阻断页面渲染。 - 平台后端:继续使用 `server-rs + Axum + SpacetimeDB`;本地开发启动独立客户端时,`agc` / Tauri dev 会先启动或复用配套 SpacetimeDB 与 `api-server`,再启动固定端口 Vite,并通过 `/api` 代理访问实际后端端口。 - 本地能力:使用 Tauri Rust command;正式用户 App 在项目运行工作台内承载 `127.0.0.1` 本地 HTTP preview,不再调用系统外部浏览器。运行容器只接受当前授权项目由 `PreviewRegistry` 返回的 loopback URL,release / dev CSP 都只为 `http://127.0.0.1:*` 开放 `frame-src`,并使用受限 iframe sandbox 隔离游戏脚本;远程 URL、`file://` 和任意手填地址均不得进入该容器。 - Agent Runtime:扩展 `server-rs/crates/platform-agent`,不引入 LangChain、AutoGen、Microsoft Agent Framework 或 OpenAI Agents SDK sidecar 作为核心。 diff --git a/packages/shared/src/components/PlatformMudPointWalletEntry/index.test.tsx b/packages/shared/src/components/PlatformMudPointWalletEntry/index.test.tsx index 0866d849e..b13ba9760 100644 --- a/packages/shared/src/components/PlatformMudPointWalletEntry/index.test.tsx +++ b/packages/shared/src/components/PlatformMudPointWalletEntry/index.test.tsx @@ -1,12 +1,6 @@ /* @vitest-environment jsdom */ -import { - act, - fireEvent, - render, - screen, - within, -} from '@testing-library/react'; +import { act, fireEvent, render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { expect, test, vi } from 'vitest'; @@ -48,7 +42,7 @@ test('shows only permanent and daily free points in the shared wallet panel', as await user.hover(balanceButton); const details = screen.getByRole('dialog', { name: '泥点账户详情' }); - expect(details.className).toContain('rounded-[1.12rem]'); + expect(details.className).toContain('rounded-[1.25rem]'); expect(within(details).getByText('不限时泥点')).toBeTruthy(); expect(within(details).getByText('按量充值、兑换码获得')).toBeTruthy(); expect(within(details).getByText('100')).toBeTruthy(); diff --git a/packages/shared/src/components/PlatformMudPointWalletEntry/index.tsx b/packages/shared/src/components/PlatformMudPointWalletEntry/index.tsx index 2d27c6d1a..f43362dc7 100644 --- a/packages/shared/src/components/PlatformMudPointWalletEntry/index.tsx +++ b/packages/shared/src/components/PlatformMudPointWalletEntry/index.tsx @@ -34,7 +34,7 @@ function MudPointBalanceRow({ detail?: string | null; }) { return ( -
+
{label} @@ -174,7 +174,7 @@ export function PlatformMudPointWalletEntry({ onBlurCapture={closeAfterFocusLeaves} >
@@ -211,7 +211,7 @@ export function PlatformMudPointWalletEntry({