diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 7d0e734d6..5c353d32e 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -6528,8 +6528,15 @@ export function App({ // The legacy Supervisor/harness path remains below for rollback and tests. if (directCodexProductRuntime) { const directInvoke = resolveTauriInvoke(); - const directProjectPath = resolveChatProjectPath(localProject); - if (directProjectPath && directInvoke) { + // Capture the project snapshot before any asynchronous policy/session work. + // `resolveChatProjectPath` only returns a path and TypeScript cannot infer + // that the source project is still non-null after an await; keeping the + // immutable snapshot also prevents a project switch from changing the + // projectId used by this turn halfway through submission. + const directProject = localProject; + const directProjectPath = resolveChatProjectPath(directProject); + const directProjectId = directProject?.manifest.projectId; + if (directProjectPath && directProjectId && directInvoke) { const clientTurnId = directConversationTurnId ?? createDirectCodexConversationTurnId(); const effectiveUserItem = diff --git a/apps/ai-game-creator-shell/src/components/WindowChrome.tsx b/apps/ai-game-creator-shell/src/components/WindowChrome.tsx index a9c5e2f29..948cf54a0 100644 --- a/apps/ai-game-creator-shell/src/components/WindowChrome.tsx +++ b/apps/ai-game-creator-shell/src/components/WindowChrome.tsx @@ -3,12 +3,14 @@ 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 { ActiveProjectRunsPanel } from '../features/app-shell/ActiveProjectRunsPanel'; import { subscribeTauriEvent } from '../services/tauriEventSubscription'; import { AppUpdateNotice } from './AppUpdateNotice'; import { WINDOW_CHROME_DEFAULT_TITLE, WindowChromeContext, type WindowChromeContextValue, + type WindowChromeActiveProjectRuns, } from './windowChromeContext'; type WindowChromeProps = { @@ -39,6 +41,8 @@ function getNativeWindow() { export function WindowChrome({ children }: WindowChromeProps) { const [title, setTitleState] = useState(WINDOW_CHROME_DEFAULT_TITLE); const [walletSlot, setWalletSlot] = useState(null); + const [activeProjectRuns, setActiveProjectRuns] = + useState(null); const setTitle = useCallback((nextTitle: string | null | undefined) => { const normalizedTitle = nextTitle?.trim(); @@ -50,6 +54,8 @@ export function WindowChrome({ children }: WindowChromeProps) { title, setTitle, walletSlot, + activeProjectRuns, + setActiveProjectRuns, }; const [isMaximized, setIsMaximized] = useState(false); @@ -142,19 +148,33 @@ export function WindowChrome({ children }: WindowChromeProps) {
- - +
+ {activeProjectRuns && + (activeProjectRuns.activeTurns.length > 0 || + activeProjectRuns.readFailed) ? ( + + ) : ( + <> +
diff --git a/apps/ai-game-creator-shell/src/components/windowChromeContext.ts b/apps/ai-game-creator-shell/src/components/windowChromeContext.ts index e84a79830..5e9141404 100644 --- a/apps/ai-game-creator-shell/src/components/windowChromeContext.ts +++ b/apps/ai-game-creator-shell/src/components/windowChromeContext.ts @@ -1,5 +1,7 @@ import { createContext, useContext } from 'react'; +import type { GameCreatorDirectActiveTurn } from '../app/types'; + export const WINDOW_CHROME_DEFAULT_TITLE = '创作工作台'; export type WindowChromeContextValue = { @@ -7,6 +9,17 @@ export type WindowChromeContextValue = { title: string; setTitle: (title: string | null | undefined) => void; walletSlot: HTMLElement | null; + activeProjectRuns: WindowChromeActiveProjectRuns | null; + setActiveProjectRuns: ( + activeProjectRuns: WindowChromeActiveProjectRuns | null, + ) => void; +}; + +export type WindowChromeActiveProjectRuns = { + activeTurns: GameCreatorDirectActiveTurn[]; + currentProjectPath?: string | null; + readFailed?: boolean; + onOpenProject?: (projectPath: string) => void; }; export const WindowChromeContext = createContext({ @@ -14,6 +27,8 @@ export const WindowChromeContext = createContext({ title: WINDOW_CHROME_DEFAULT_TITLE, setTitle: () => undefined, walletSlot: null, + activeProjectRuns: null, + setActiveProjectRuns: () => undefined, }); export function useWindowChrome() { diff --git a/apps/ai-game-creator-shell/src/features/app-shell/AccountWallet.tsx b/apps/ai-game-creator-shell/src/features/app-shell/AccountWallet.tsx index 3a9933491..33096e243 100644 --- a/apps/ai-game-creator-shell/src/features/app-shell/AccountWallet.tsx +++ b/apps/ai-game-creator-shell/src/features/app-shell/AccountWallet.tsx @@ -1,6 +1,7 @@ import { PlatformMudPointWalletEntry } from '../../../../../packages/shared/src/components/PlatformMudPointWalletEntry'; import { PlatformProfileRechargeModal } from '../../../../../packages/shared/src/components/PlatformProfileRechargeModal'; import { PlatformProfileWalletLedgerModal } from '../../../../../packages/shared/src/components/PlatformProfileWalletLedgerModal'; +import { ThemedModal } from '../../components/modal/ThemedModal'; import type { AccountWalletController } from './useAccountWallet'; export function AccountWalletBar({ @@ -21,6 +22,7 @@ export function AccountWalletBar({ onRequestDetails={() => void controller.onWalletBalanceMayHaveChanged()} onRecharge={controller.openRecharge} onOpenLedger={controller.openWalletLedger} + onRedeemCode={controller.openRedeemCode} />
); @@ -63,6 +65,37 @@ export function AccountWalletDialogs({ onRetry={() => void controller.loadWalletLedger()} /> ) : null} + +
+ 兑换码 + +
+
{ + event.preventDefault(); + void controller.redeemCode(); + }} + > + controller.setRedeemCodeInput(event.target.value)} + placeholder="输入兑换码" + aria-label="兑换码" + autoFocus + /> + {controller.redeemCodeError ?

{controller.redeemCodeError}

: null} + {controller.redeemCodeSuccess ?

{controller.redeemCodeSuccess}

: null} + +
+
); } diff --git a/apps/ai-game-creator-shell/src/features/app-shell/ActiveProjectRunsPanel.tsx b/apps/ai-game-creator-shell/src/features/app-shell/ActiveProjectRunsPanel.tsx index 4147abb9b..c51a6fa49 100644 --- a/apps/ai-game-creator-shell/src/features/app-shell/ActiveProjectRunsPanel.tsx +++ b/apps/ai-game-creator-shell/src/features/app-shell/ActiveProjectRunsPanel.tsx @@ -1,9 +1,12 @@ +import { ChevronDown } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; + import type { GameCreatorDirectActiveTurn } from '../../app/types'; import { projectNameFromPath } from '../agent-runtime'; import { projectPathsMatchForInvalidation } from '../project-summary/projectPath'; /** - * 左上角的"正在运行的项目"面板。 + * 窗口标题栏的"正在运行的项目"入口,也保留面板布局供独立组件测试和复用。 * * 数据来自 Rust 的活动回合注册表(同一个只读快照也用于重新进入项目时的进度重连), * 面板只负责呈现:项目名、阶段、已运行时长,以及点击进入该项目。没有在跑回合时 @@ -14,6 +17,7 @@ export type ActiveProjectRunsPanelProps = { currentProjectPath?: string | null; readFailed?: boolean; onOpenProject?: (projectPath: string) => void; + placement?: 'panel' | 'titlebar'; }; const ACTIVE_TURN_STATUS_LABELS: Record = { @@ -56,11 +60,47 @@ export function ActiveProjectRunsPanel({ currentProjectPath = null, readFailed = false, onOpenProject, + placement = 'panel', }: ActiveProjectRunsPanelProps) { + const [open, setOpen] = useState(false); + const menuRef = useRef(null); + + useEffect(() => { + if (!open || placement !== 'titlebar') { + return; + } + const handlePointerDown = (event: PointerEvent) => { + if (!menuRef.current?.contains(event.target as Node)) { + setOpen(false); + } + }; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setOpen(false); + } + }; + document.addEventListener('pointerdown', handlePointerDown); + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('pointerdown', handlePointerDown); + document.removeEventListener('keydown', handleKeyDown); + }; + }, [open, placement]); + if (activeTurns.length === 0) { if (!readFailed) { return null; } + if (placement === 'titlebar') { + return ( + + 正在运行的项目读取失败 + + ); + } // 三次都没读到快照:只说"没读到",不改写成业务、权限或审批结论。 return (