新增总控Agent纯聊天开发窗口
新增开发态独立窗口路由与专业Agent开发页入口 固定复用project-supervisor活动会话、Runtime和持久对话 提供纯消息区、流式等待状态、输入框与运行时设置 补齐窗口编码、前端交互、开发态门禁和原生壳验证 同步AI游戏创作实施计划与项目决策记录
This commit is contained in:
@@ -646,6 +646,10 @@ for (const snippet of [
|
||||
'open_developer_window(app.handle())?',
|
||||
'tauri::WebviewWindowBuilder::new(app, "developer", developer_window_url())',
|
||||
'index.html?agent-chat',
|
||||
'supervisorChatMode',
|
||||
'supervisorChatOnly',
|
||||
'open_project_supervisor_chat_window',
|
||||
'index.html?supervisor-chat&projectPath=',
|
||||
]) {
|
||||
if (!`${appEntrypointSource}\n${tauriRustSource}`.includes(snippet)) {
|
||||
throw new Error(
|
||||
|
||||
@@ -1749,6 +1749,7 @@ fn main() {
|
||||
write_project_permission_policy,
|
||||
open_game_creator_workspace_window,
|
||||
open_game_creator_launcher_window,
|
||||
open_project_supervisor_chat_window,
|
||||
start_local_game_preview,
|
||||
open_local_game_preview,
|
||||
stop_local_game_preview,
|
||||
|
||||
@@ -48860,6 +48860,14 @@ fn developer_window_uses_agent_chat_route() {
|
||||
assert_eq!(developer_window_url().to_string(), "index.html?agent-chat");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supervisor_chat_window_carries_encoded_project_path() {
|
||||
assert_eq!(
|
||||
supervisor_chat_window_url("/tmp/AI Game 项目").to_string(),
|
||||
"index.html?supervisor-chat&projectPath=%2Ftmp%2FAI%20Game%20%E9%A1%B9%E7%9B%AE"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_window_url_carries_encoded_project_path() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -15,6 +15,13 @@ pub(crate) fn developer_window_url() -> tauri::WebviewUrl {
|
||||
tauri::WebviewUrl::App(PathBuf::from("index.html?agent-chat"))
|
||||
}
|
||||
|
||||
pub(crate) fn supervisor_chat_window_url(project_path: &str) -> tauri::WebviewUrl {
|
||||
tauri::WebviewUrl::App(PathBuf::from(format!(
|
||||
"index.html?supervisor-chat&projectPath={}",
|
||||
percent_encode_query_value(project_path)
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) fn validate_workspace_window_project_path(project_path: &str) -> Result<&str, String> {
|
||||
let project_path = project_path.trim();
|
||||
if project_path.is_empty() {
|
||||
@@ -81,6 +88,37 @@ pub(crate) fn open_game_creator_launcher_window(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn open_project_supervisor_chat_window(
|
||||
app: tauri::AppHandle,
|
||||
project_path: String,
|
||||
) -> Result<(), String> {
|
||||
let project_path = validate_workspace_window_project_path(&project_path)?;
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
let _ = app;
|
||||
let _ = project_path;
|
||||
return Err("项目总控对话窗口仅在开发构建中可用".to_string());
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if let Some(existing) = app.get_webview_window("supervisor-chat") {
|
||||
existing.close().map_err(|error| error.to_string())?;
|
||||
}
|
||||
tauri::WebviewWindowBuilder::new(
|
||||
&app,
|
||||
"supervisor-chat",
|
||||
supervisor_chat_window_url(project_path),
|
||||
)
|
||||
.title("项目总控 Agent 对话")
|
||||
.inner_size(820.0, 720.0)
|
||||
.min_inner_size(560.0, 480.0)
|
||||
.build()
|
||||
.map_err(|error| error.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(debug_assertions, not(test)))]
|
||||
pub(crate) fn open_developer_window(app: &tauri::AppHandle) -> Result<(), String> {
|
||||
if let Some(existing) = app.get_webview_window("developer") {
|
||||
|
||||
@@ -1642,6 +1642,22 @@ function agentRuntimeConversationStatus(runtime: AgentRuntimeState) {
|
||||
return waitingOn ? `Agent 正在运行,等待${waitingOn}` : 'Agent 正在运行';
|
||||
}
|
||||
|
||||
function projectSupervisorChatRuntimeStatus(runtime: AgentRuntimeState) {
|
||||
if (runtime.status === 'idle' || runtime.phase === 'idle') {
|
||||
return '等待输入';
|
||||
}
|
||||
if (isAgentRuntimeTerminalState(runtime)) {
|
||||
if (runtime.status === 'failed' || runtime.phase === 'failed') {
|
||||
return runtime.error || 'Agent 运行失败';
|
||||
}
|
||||
if (runtime.status === 'cancelled' || runtime.phase === 'cancelled') {
|
||||
return '本轮已取消';
|
||||
}
|
||||
return '本轮已完成';
|
||||
}
|
||||
return agentRuntimeConversationStatus(runtime);
|
||||
}
|
||||
|
||||
function formatAgentRuntimeEvent(event: AgentRuntimeEventRecord) {
|
||||
const summary = event.summary || event.detail || event.runId;
|
||||
const detail =
|
||||
@@ -6561,6 +6577,32 @@ export function WorkspaceLauncher({
|
||||
agentChatGoalDialog !== null && !agentChatBackgroundBusy,
|
||||
);
|
||||
|
||||
async function handleOpenProjectSupervisorChatWindow() {
|
||||
if (agentChatBusy || agentChatBackgroundBusy) {
|
||||
return;
|
||||
}
|
||||
const projectPathForChat = validateAgentChatProjectPath();
|
||||
if (!projectPathForChat) {
|
||||
return;
|
||||
}
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) {
|
||||
setAgentChatStatus('需要在 Tauri App 内运行');
|
||||
return;
|
||||
}
|
||||
setAgentChatStatus('正在打开项目总控对话');
|
||||
try {
|
||||
await invoke('open_project_supervisor_chat_window', {
|
||||
projectPath: projectPathForChat,
|
||||
});
|
||||
setAgentChatStatus('已打开项目总控对话');
|
||||
} catch (error) {
|
||||
setAgentChatStatus(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAgentChatPickProjectDirectory() {
|
||||
if (agentChatBusy || agentChatBackgroundBusy) {
|
||||
return;
|
||||
@@ -8896,6 +8938,13 @@ export function WorkspaceLauncher({
|
||||
<p>{agentChatStatus}</p>
|
||||
</div>
|
||||
<div className="launcher-project-list-actions">
|
||||
<button
|
||||
type="button"
|
||||
disabled={agentChatBusy || agentChatBackgroundBusy}
|
||||
onClick={() => void handleOpenProjectSupervisorChatWindow()}
|
||||
>
|
||||
总控对话
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={agentChatBusy || agentChatBackgroundBusy}
|
||||
@@ -16974,12 +17023,14 @@ type AppProps = {
|
||||
initialProjectPath?: string;
|
||||
initialProjectManifest?: GameCreationAppManifest;
|
||||
projectSupervisorOnly?: boolean;
|
||||
supervisorChatOnly?: boolean;
|
||||
};
|
||||
|
||||
export function App({
|
||||
initialProjectPath: initialProjectPathOverride = '',
|
||||
initialProjectManifest,
|
||||
projectSupervisorOnly = false,
|
||||
supervisorChatOnly = false,
|
||||
}: AppProps = {}) {
|
||||
const [devMode] = useState(() =>
|
||||
projectSupervisorOnly ? false : isDeveloperMode(),
|
||||
@@ -17022,6 +17073,8 @@ export function App({
|
||||
const [projectSupervisorRuntimeError, setProjectSupervisorRuntimeError] =
|
||||
useState('');
|
||||
const chatInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const supervisorChatMessagesRef = useRef<HTMLDivElement | null>(null);
|
||||
const supervisorChatShouldFollowLatestRef = useRef(true);
|
||||
const [assetStatus, setAssetStatus] = useState('未上传');
|
||||
const [uploadedAssets, setUploadedAssets] = useState<
|
||||
UploadLocalAssetResult[]
|
||||
@@ -17178,7 +17231,7 @@ export function App({
|
||||
const pendingUiConfirmationActionRef = useRef<(() => void) | null>(null);
|
||||
|
||||
function requestRuntimeConfigOpen() {
|
||||
if (projectSupervisorOnly) {
|
||||
if (projectSupervisorOnly && !supervisorChatOnly) {
|
||||
return;
|
||||
}
|
||||
setRuntimeConfigOpen(true);
|
||||
@@ -17543,6 +17596,22 @@ export function App({
|
||||
projectSupervisorRuntime?.status,
|
||||
]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!supervisorChatOnly || !supervisorChatShouldFollowLatestRef.current) {
|
||||
return;
|
||||
}
|
||||
const messageList = supervisorChatMessagesRef.current;
|
||||
if (messageList) {
|
||||
messageList.scrollTop = messageList.scrollHeight;
|
||||
}
|
||||
}, [
|
||||
messages,
|
||||
projectSupervisorResponseStream?.sequence,
|
||||
projectSupervisorRuntime?.updatedAt,
|
||||
projectSupervisorRuntimeError,
|
||||
supervisorChatOnly,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
latestMessagesRef.current = messages;
|
||||
const invoke = resolveTauriInvoke();
|
||||
@@ -26599,6 +26668,17 @@ export function App({
|
||||
}
|
||||
}
|
||||
|
||||
function handleSupervisorChatScroll(event: UIEvent<HTMLDivElement>) {
|
||||
handleConversationScroll(event);
|
||||
const messageList = event.currentTarget;
|
||||
const distanceFromBottom =
|
||||
messageList.scrollHeight -
|
||||
messageList.scrollTop -
|
||||
messageList.clientHeight;
|
||||
supervisorChatShouldFollowLatestRef.current =
|
||||
distanceFromBottom <= AGENT_CHAT_SCROLL_BOTTOM_THRESHOLD;
|
||||
}
|
||||
|
||||
function handleAgentConversationScroll(event: UIEvent<HTMLElement>) {
|
||||
if (hiddenAgentConversationCount === 0) {
|
||||
return;
|
||||
@@ -26636,6 +26716,9 @@ export function App({
|
||||
if (!prompt || chatAgentBusy) {
|
||||
return;
|
||||
}
|
||||
if (supervisorChatOnly) {
|
||||
supervisorChatShouldFollowLatestRef.current = true;
|
||||
}
|
||||
setChatInput('');
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
@@ -26650,6 +26733,132 @@ export function App({
|
||||
(agent.runtimeStatus !== null || agent.hasRecentEvidence),
|
||||
);
|
||||
|
||||
if (projectSupervisorOnly && supervisorChatOnly) {
|
||||
const projectSupervisorChatRunning = Boolean(
|
||||
chatAgentBusy ||
|
||||
(projectSupervisorRuntime &&
|
||||
!isAgentRuntimeTerminalState(projectSupervisorRuntime)),
|
||||
);
|
||||
const projectSupervisorChatStatus = projectSupervisorRuntimeError
|
||||
? projectSupervisorRuntimeError
|
||||
: projectSupervisorRuntime
|
||||
? projectSupervisorChatRuntimeStatus(projectSupervisorRuntime)
|
||||
: workspaceStatus;
|
||||
const supervisorProjectPath =
|
||||
localProject?.projectPath || initialProjectPath || projectPath;
|
||||
return (
|
||||
<>
|
||||
<main
|
||||
className="supervisor-chat-only-shell"
|
||||
aria-label="项目总控 Agent 纯聊天"
|
||||
>
|
||||
<header className="supervisor-chat-only-header">
|
||||
<div>
|
||||
<strong>项目总控 Agent</strong>
|
||||
<span>{projectNameFromPath(supervisorProjectPath)}</span>
|
||||
</div>
|
||||
<div className="supervisor-chat-only-header-status">
|
||||
<span aria-live="polite">{projectSupervisorChatStatus}</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="设置"
|
||||
title="设置"
|
||||
onClick={() => setRuntimeConfigOpen(true)}
|
||||
>
|
||||
<Settings size={17} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
ref={supervisorChatMessagesRef}
|
||||
className="message-list supervisor-chat-only-message-list"
|
||||
aria-label="项目总控消息"
|
||||
onScroll={handleSupervisorChatScroll}
|
||||
>
|
||||
{hiddenConversationCount > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
className="message-history-more"
|
||||
onClick={showEarlierConversationMessages}
|
||||
>
|
||||
{`显示更早 · 还有 ${hiddenConversationCount} 条对话`}
|
||||
</button>
|
||||
) : null}
|
||||
{visibleMessages.map((message, index) => (
|
||||
<p
|
||||
key={message.messageId ?? `${message.role}-${index}`}
|
||||
className={`message message--${message.role}`}
|
||||
>
|
||||
{message.text}
|
||||
</p>
|
||||
))}
|
||||
{projectSupervisorTransientReply ? (
|
||||
<p
|
||||
className="message message--assistant supervisor-chat-only-stream"
|
||||
aria-label="项目总控 Agent 实时回复"
|
||||
aria-live="polite"
|
||||
data-runtime-owned="true"
|
||||
>
|
||||
{projectSupervisorTransientReply}
|
||||
</p>
|
||||
) : null}
|
||||
{projectSupervisorChatRunning &&
|
||||
!projectSupervisorTransientReply ? (
|
||||
<div
|
||||
className="launcher-agent-chat-waiting supervisor-chat-only-waiting"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<span aria-hidden="true" />
|
||||
<div>
|
||||
<strong>{projectSupervisorChatStatus}</strong>
|
||||
<small>总控 Agent 正在继续处理</small>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<form
|
||||
className="supervisor-chat-only-composer"
|
||||
onSubmit={handleProjectSupervisorOnlySubmit}
|
||||
>
|
||||
<textarea
|
||||
aria-label="项目总控对话内容"
|
||||
disabled={chatAgentBusy || projectSupervisorNeedsUserInput}
|
||||
rows={3}
|
||||
value={chatInput}
|
||||
placeholder="给项目总控 Agent 发消息"
|
||||
onChange={(event) => setChatInput(event.currentTarget.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
event.key === 'Enter' &&
|
||||
!event.shiftKey &&
|
||||
!event.nativeEvent.isComposing
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.currentTarget.form?.requestSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
aria-label="发送"
|
||||
title="发送"
|
||||
disabled={chatAgentBusy || projectSupervisorNeedsUserInput}
|
||||
>
|
||||
<Send size={18} aria-hidden="true" />
|
||||
</button>
|
||||
</form>
|
||||
</main>
|
||||
{runtimeConfigOpen ? (
|
||||
<RuntimeConfigDialog
|
||||
projectPath={supervisorProjectPath}
|
||||
onClose={() => setRuntimeConfigOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (projectSupervisorOnly) {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { AuthenticatedClient, WorkspaceLauncher } from './App';
|
||||
import { App, AuthenticatedClient, WorkspaceLauncher } from './App';
|
||||
import './styles.css';
|
||||
|
||||
const initialSearchParams = new URLSearchParams(window.location.search);
|
||||
const supervisorChatMode =
|
||||
import.meta.env.DEV && initialSearchParams.has('supervisor-chat');
|
||||
const supervisorChatProjectPath =
|
||||
initialSearchParams.get('projectPath')?.trim() ?? '';
|
||||
|
||||
function resolveInitialLauncherView() {
|
||||
return import.meta.env.DEV &&
|
||||
new URLSearchParams(window.location.search).has('agent-chat')
|
||||
return import.meta.env.DEV && initialSearchParams.has('agent-chat')
|
||||
? 'agent-chat'
|
||||
: 'home';
|
||||
}
|
||||
@@ -16,13 +21,21 @@ const initialLauncherView = resolveInitialLauncherView();
|
||||
createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<AuthenticatedClient>
|
||||
{({ user, logout }) => (
|
||||
<WorkspaceLauncher
|
||||
currentUser={user}
|
||||
initialView={initialLauncherView}
|
||||
onLogout={logout}
|
||||
/>
|
||||
)}
|
||||
{({ user, logout }) =>
|
||||
supervisorChatMode ? (
|
||||
<App
|
||||
initialProjectPath={supervisorChatProjectPath}
|
||||
projectSupervisorOnly
|
||||
supervisorChatOnly
|
||||
/>
|
||||
) : (
|
||||
<WorkspaceLauncher
|
||||
currentUser={user}
|
||||
initialView={initialLauncherView}
|
||||
onLogout={logout}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</AuthenticatedClient>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -1480,6 +1480,188 @@ textarea {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-shell {
|
||||
display: grid;
|
||||
grid-template-rows: 64px minmax(0, 1fr) auto;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
min-width: 0;
|
||||
min-height: 480px;
|
||||
overflow: hidden;
|
||||
background: #f5f6f8;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
padding: 0 18px;
|
||||
border-bottom: 1px solid #e1e5eb;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-header > div,
|
||||
.supervisor-chat-only-header-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-header > div:first-child {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-header strong {
|
||||
flex: 0 0 auto;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-header span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-header-status {
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
max-width: 52%;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-header-status button {
|
||||
display: grid;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
flex: 0 0 auto;
|
||||
padding: 0;
|
||||
border: 1px solid #d8dde5;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
color: #374151;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-message-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 22px clamp(16px, 4vw, 40px);
|
||||
border: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-message-list .message {
|
||||
align-self: flex-start;
|
||||
width: fit-content;
|
||||
max-width: min(720px, 86%);
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #dfe3e9;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #263142;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-message-list .message--user {
|
||||
align-self: flex-end;
|
||||
border-color: #cfd6df;
|
||||
background: #e9edf2;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-message-list .supervisor-chat-only-stream {
|
||||
border-color: #b9d8c5;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-waiting {
|
||||
position: static;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-composer {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 42px;
|
||||
align-items: end;
|
||||
gap: 10px;
|
||||
padding: 14px clamp(16px, 4vw, 40px) 18px;
|
||||
border-top: 1px solid #e1e5eb;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-composer textarea {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 74px;
|
||||
max-height: 74px;
|
||||
padding: 11px 12px;
|
||||
overflow-y: auto;
|
||||
resize: none;
|
||||
border: 1px solid #cfd6df;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
font: inherit;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-composer textarea:focus-visible {
|
||||
border-color: #6b7280;
|
||||
outline: 2px solid rgb(107 114 128 / 18%);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-composer button {
|
||||
display: grid;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: #111827;
|
||||
color: #fff;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-composer button:disabled,
|
||||
.supervisor-chat-only-composer textarea:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.supervisor-chat-only-header {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-header > div:first-child > span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-header-status {
|
||||
max-width: 58%;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-message-list .message {
|
||||
max-width: 92%;
|
||||
}
|
||||
|
||||
.supervisor-chat-only-composer {
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.project-supervisor-surface {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(220px, 280px);
|
||||
|
||||
@@ -1623,6 +1623,45 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('opens the standalone Project Supervisor chat window with the absolute project path', async () => {
|
||||
const projectPath = '/tmp/supervisor-chat-window-game';
|
||||
const invoke = vi.fn(async (command: string) => {
|
||||
if (command === 'check_game_creator_llm_config') {
|
||||
return {
|
||||
configured: true,
|
||||
apiKeyPresent: true,
|
||||
baseUrl: 'https://llm.example.test/v1',
|
||||
model: 'gpt-test',
|
||||
apiKind: 'openai_responses',
|
||||
reasoningEffort: 'high',
|
||||
stream: true,
|
||||
webSearchEnabled: false,
|
||||
error: null,
|
||||
agents: [],
|
||||
};
|
||||
}
|
||||
if (command === 'open_project_supervisor_chat_window') {
|
||||
return null;
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
renderLauncherAgentChatAt('/?agent-chat');
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), {
|
||||
target: { value: projectPath },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '总控对话' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(invoke).toHaveBeenCalledWith(
|
||||
'open_project_supervisor_chat_window',
|
||||
{ projectPath },
|
||||
);
|
||||
});
|
||||
expect(screen.getByText('已打开项目总控对话')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('persists and shows an upstream stream error without issuing a normal retry', async () => {
|
||||
const persistedMessages: Array<{
|
||||
role: 'user' | 'assistant' | 'tool';
|
||||
@@ -6065,6 +6104,81 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('loads and continues the active Project Supervisor Session in the standalone chat surface', async () => {
|
||||
const projectPath = '/tmp/supervisor-chat-only-game';
|
||||
const historyMessage = '已持久化的项目总控历史';
|
||||
const harness = createProjectSupervisorRuntimeHarness({
|
||||
projectPath,
|
||||
supervisorMessages: [
|
||||
{
|
||||
schemaVersion: 'game-creator-conversation.v1',
|
||||
role: 'assistant',
|
||||
content: historyMessage,
|
||||
agentId: 'project-supervisor',
|
||||
messageId: 'supervisor-chat-only-history',
|
||||
updatedAt: 2000,
|
||||
},
|
||||
],
|
||||
});
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: harness.invoke },
|
||||
event: { listen: harness.listen },
|
||||
};
|
||||
render(
|
||||
React.createElement(App, {
|
||||
initialProjectPath: projectPath,
|
||||
projectSupervisorOnly: true,
|
||||
supervisorChatOnly: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const surface = await screen.findByLabelText('项目总控 Agent 纯聊天');
|
||||
const messageList = within(surface).getByLabelText('项目总控消息');
|
||||
expect(await within(messageList).findByText(historyMessage)).not.toBeNull();
|
||||
expect(harness.invoke).toHaveBeenCalledWith(
|
||||
'list_game_creator_agent_sessions',
|
||||
{
|
||||
projectPath,
|
||||
agentId: 'project-supervisor',
|
||||
},
|
||||
);
|
||||
expect(harness.invoke).toHaveBeenCalledWith('read_local_conversation', {
|
||||
projectPath,
|
||||
agentId: 'project-supervisor',
|
||||
sessionId: harness.sessionId,
|
||||
});
|
||||
expect(within(surface).getByRole('button', { name: '设置' })).not.toBeNull();
|
||||
expect(screen.queryByLabelText('选择 Agent')).toBeNull();
|
||||
expect(screen.queryByLabelText('项目总控 Agent 状态')).toBeNull();
|
||||
expect(screen.queryByLabelText('专业 Agent 协作状态')).toBeNull();
|
||||
|
||||
fireEvent.change(within(surface).getByLabelText('项目总控对话内容'), {
|
||||
target: { value: '继续完成可玩原型' },
|
||||
});
|
||||
fireEvent.click(within(surface).getByRole('button', { name: '发送' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(harness.invoke).toHaveBeenCalledWith(
|
||||
'start_game_creator_agent_runtime_task',
|
||||
{
|
||||
projectPath,
|
||||
agentId: 'project-supervisor',
|
||||
sessionId: harness.sessionId,
|
||||
task: '继续完成可玩原型',
|
||||
runId: expect.stringMatching(/^project-supervisor-task-/),
|
||||
},
|
||||
);
|
||||
});
|
||||
expect(harness.invoke).not.toHaveBeenCalledWith(
|
||||
'create_game_creator_agent_session',
|
||||
expect.anything(),
|
||||
);
|
||||
expect(harness.invoke).not.toHaveBeenCalledWith(
|
||||
'chat_with_game_creator_agent',
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('opens an existing project into the active Supervisor Session, restores history, then starts and steers the same run', async () => {
|
||||
const projectPath = '/tmp/launcher-supervisor-game';
|
||||
const manifest = createGameCreationAppManifest(
|
||||
|
||||
@@ -5118,3 +5118,11 @@
|
||||
- 决策:旧创作模板、旧创作入口及其专属运行服务进入下线范围,不再为跳一跳、抓大鹅 Match3D、儿童动作 Demo 等旧链路修复兼容问题、补生成脚本或维持专属门禁。
|
||||
- 边界:共享账号、钱包、资产、图片编辑器、公开作品、通用 HostBridge、API、SpacetimeDB、发布运维和安全能力不属于旧链路,仍需维持正式门禁。历史文档只作为背景材料,不再作为旧入口继续运行的依据。
|
||||
- 清理方式:允许直接删除已经失效的旧素材生成命令、入口路由、专属服务和对应测试;删除工程链路时仍需核对是否被当前共享能力引用,不能连带移除仍在使用的公共契约或持久化事实。
|
||||
|
||||
## 2026-07-20 开发态 Project Supervisor 纯聊天独立窗口
|
||||
|
||||
- 背景:开发人员需要一个不依赖正式产品布局的最小 GUI,用于直接验证 `project-supervisor` 的持久多轮对话与 Runtime 行为。
|
||||
- 入口决策:仅 debug 构建提供独立 Tauri 窗口,使用 `index.html?supervisor-chat&projectPath=...` 路由,并从现有 `index.html?agent-chat` 开发入口打开;`projectPath` 是 URL 编码后的项目绝对路径。
|
||||
- 复用边界:窗口固定使用 `project-supervisor`,继续复用现有 active Session、External Runner、Agent Runtime、AppData 配置与持久 conversation;不创建新聊天后端、本地 HTTP 服务、数据库或平行配置体系。
|
||||
- UI 边界:只显示持久消息区、输入框、必要的等待 / 错误状态和设置;不显示 Agent picker、Session / Goal / Runtime 面板或专业 Agent 协作栏。Session 控制面可隐藏,但对话仍按 `project-supervisor` active Session 持久化。
|
||||
- 产品边界:该窗口只是开发验证入口,不替换、不修改正式用户 `client` 窗口及其登录、首页和项目开发流程。
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
- Run 控制参考:借鉴 Harbour 的控制平面思想,只吸收 `run lifecycle`、activity/output stream、context bundle、kill/retry/resume 等本地运行治理能力;不引入 Harbour 的多租户后台、调度 UI、通用 shell workflow 或远程 runner 作为 v1 依赖。
|
||||
- 本地工程参考:借鉴 Godcoder 的本地产物 checkpoint / diff / restore、上下文安全过滤、轻量项目索引、项目级写锁和项目级权限策略;不引入通用 IDE 插件、云工作区或任意代码代理。
|
||||
|
||||
## 开发态 Project Supervisor 纯聊天独立窗口
|
||||
|
||||
- 入口边界:仅 debug 构建提供独立 Tauri 窗口,路由为 `index.html?supervisor-chat&projectPath=...`,其中 `projectPath` 传入 URL 编码后的项目绝对路径。窗口从现有 `index.html?agent-chat` 开发入口打开,不增加正式用户入口。
|
||||
- Runtime 边界:窗口固定对话 Agent 为 `project-supervisor`,复用现有 active Session、External Runner、Agent Runtime、AppData 配置和持久 conversation;不新建平行会话库、Runner 或配置存储。
|
||||
- 界面边界:只显示持久消息区、输入框、必要的等待 / 错误状态和设置入口;不显示 Agent picker、Session 面板、Goal 面板、Runtime 面板或专业 Agent 协作栏。会话历史仍绑定 `project-supervisor` 的 active Session 持久化,不因隐藏 Session 控制面而变为临时聊天。
|
||||
- 产品边界:正式用户 `client` 窗口、登录后首页和项目开发流程保持不变,不暴露该开发验证面。
|
||||
|
||||
## Runtime 边界
|
||||
|
||||
V1.11 的受保护仓库控制目录同时包含 `.git / .agent / .agents / .codex / .hermes`;其中 `.agent` 对项目命令隐藏,其余控制目录只读。
|
||||
|
||||
@@ -1986,6 +1986,18 @@ function assertAiGameCreatorShellUserDevBoundary() {
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const snippet of [
|
||||
'fn open_project_supervisor_chat_window(',
|
||||
'#[cfg(not(debug_assertions))]',
|
||||
'项目总控对话窗口仅在开发构建中可用',
|
||||
'index.html?supervisor-chat&projectPath=',
|
||||
]) {
|
||||
if (!aiGameCreatorShellTauriSource.includes(snippet)) {
|
||||
throw new Error(
|
||||
`AI game creator supervisor chat window must stay developer-only: ${snippet}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceWindowCommandIndex = aiGameCreatorShellTauriSource.indexOf(
|
||||
'fn open_game_creator_workspace_window(',
|
||||
|
||||
Reference in New Issue
Block a user