Files
Genarrative/apps/ai-game-creator-shell/src/features/app-shell/DeveloperAgentPanel.tsx
T
AIGameCreator App be89296492 拆分 AI 游戏创作客户端大型模块
拆分 App 认证、壳层、运行配置与项目摘要模块
拆分 Tauri 项目能力与 Rust 测试领域模块
拆分界面测试与 Agent Runtime 真实 E2E 套件
补充源码扫描和客户端模块化文档约定
2026-07-21 22:53:29 +08:00

735 lines
26 KiB
TypeScript

import { Archive, GitFork, Plus } from 'lucide-react';
import { closeDialogOnEscape } from '../../app/dialogs';
import type { AgentBackgroundSubmitMode } from '../../app/types';
import {
AgentGoalStatusPanel,
AgentRuntimeStatusPanel,
formatAgentConversationSessionMeta,
} from '../agent-runtime';
import {
taskGroupLabels,
taskStatusLabels,
} from '../project-summary/projectSummary';
import type { DeveloperAgentPanelController } from './useDeveloperAgentPanel';
export function DeveloperAgentPanel({
controller,
onRuntimeConfigOpen,
}: {
controller: DeveloperAgentPanelController;
onRuntimeConfigOpen: () => void;
}) {
const {
agentChatActiveSessionId,
agentChatBackgroundBusy,
agentChatBusy,
agentChatConversationPath,
agentChatGoal,
agentChatGoalError,
agentChatInput,
agentChatInteractionMode,
agentChatLegacySessionMode,
agentChatLlmStatus,
agentChatLoadVersionRef,
agentChatMessages,
agentChatMessagesRef,
agentChatProjectPath,
agentChatResumeConfirmation,
agentChatRunSubmitMode,
agentChatRuntime,
agentChatRuntimeError,
agentChatRuntimeResumeProjectPathRef,
agentChatSelectedAgentId,
agentChatSelectedSessionId,
agentChatSessionStatus,
agentChatSessions,
agentChatStatus,
cancelAgentChatRuntimeResume,
confirmAgentChatRuntimeResume,
currentAgentChatActiveSessions,
currentAgentChatAgent,
currentAgentChatArchivedSessions,
currentAgentChatGoalDialogMode,
currentAgentChatGoalStatus,
currentAgentChatLlmWarning,
currentAgentChatNeedsUserInput,
currentAgentChatSession,
currentAgentChatSessionArchived,
currentAgentChatSessionMutationBlocked,
currentAgentChatSteerRuntime,
currentAgentChatWaiting,
currentAgentChatWaitingDetail,
currentAgentChatWaitingStatus,
handleAgentChatArchiveSession,
handleAgentChatCancelRuntimeTask,
handleAgentChatCompactContext,
handleAgentChatConfirmRuntimeTask,
handleAgentChatCreateSession,
handleAgentChatForkSession,
handleAgentChatGoalControl,
handleAgentChatMessagesScroll,
handleAgentChatPickProjectDirectory,
handleAgentChatRejectRuntimeTask,
handleAgentChatRetryRuntimeTask,
handleAgentChatSelectSession,
handleAgentChatStartBackgroundTask,
handleAgentChatSubmit,
handleAgentChatSubmitUserInput,
handleOpenProjectSupervisorChatWindow,
launcherAgentChatAgents,
loadAgentChatConversation,
loadAgentChatLlmStatus,
openAgentChatGoalDialog,
resetAgentChatSessionView,
setAgentChatInput,
setAgentChatInteractionMode,
setAgentChatProjectPath,
setAgentChatResumeConfirmation,
setAgentChatRunSubmitMode,
setAgentChatSelectedAgentId,
setAgentChatStatus,
} = controller;
return (
<section className="launcher-page launcher-agent-chat-page">
<header>
<div>
<h1>Agent 聊天</h1>
<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}
onClick={() => void loadAgentChatLlmStatus()}
>
LLM状态
</button>
<button type="button" onClick={() => onRuntimeConfigOpen()}>
配置
</button>
<button
type="button"
disabled={agentChatBusy || agentChatBackgroundBusy}
onClick={() =>
void loadAgentChatConversation(
agentChatSelectedAgentId,
agentChatProjectPath,
agentChatSelectedSessionId,
)
}
>
读取历史
</button>
</div>
</header>
<section className="launcher-agent-chat-layout">
<aside className="launcher-agent-chat-sidebar">
<form
className="launcher-agent-chat-project"
onSubmit={(event) => {
event.preventDefault();
void loadAgentChatConversation(
agentChatSelectedAgentId,
agentChatProjectPath,
agentChatSelectedSessionId,
);
}}
>
<label>
项目目录
<input
aria-label="Agent 聊天项目目录"
disabled={agentChatBusy || agentChatBackgroundBusy}
value={agentChatProjectPath}
onChange={(event) => {
agentChatLoadVersionRef.current += 1;
agentChatRuntimeResumeProjectPathRef.current = null;
setAgentChatResumeConfirmation(null);
setAgentChatProjectPath(event.currentTarget.value);
resetAgentChatSessionView();
setAgentChatStatus('请选择并读取会话');
}}
/>
</label>
<div className="launcher-page-actions">
<button
type="button"
disabled={agentChatBusy || agentChatBackgroundBusy}
onClick={() => void handleAgentChatPickProjectDirectory()}
>
选择
</button>
<button
type="submit"
disabled={agentChatBusy || agentChatBackgroundBusy}
>
读取
</button>
</div>
</form>
<div className="launcher-agent-picker" aria-label="选择 Agent">
{launcherAgentChatAgents.map((agent) => (
<button
key={agent.id}
type="button"
disabled={agentChatBusy || agentChatBackgroundBusy}
className={
agent.id === currentAgentChatAgent?.id
? 'launcher-agent-active'
: ''
}
onClick={() => {
agentChatLoadVersionRef.current += 1;
setAgentChatSelectedAgentId(agent.id);
resetAgentChatSessionView();
void loadAgentChatConversation(agent.id);
}}
>
<strong>{agent.title}</strong>
<span>{`${taskGroupLabels[agent.group]} / ${agent.role}`}</span>
<small>{agent.summary}</small>
</button>
))}
</div>
</aside>
<section className="launcher-agent-chat-main">
<header>
<div>
<h2>{currentAgentChatAgent?.title ?? '未选择 Agent'}</h2>
{currentAgentChatAgent ? (
<p>{`${taskGroupLabels[currentAgentChatAgent.group]} / ${currentAgentChatAgent.role} · ${taskStatusLabels[currentAgentChatAgent.status]}`}</p>
) : null}
<p>{agentChatLlmStatus}</p>
</div>
<small>{agentChatConversationPath || '请选择并读取会话'}</small>
</header>
<section
className="launcher-agent-session-bar"
aria-label="Agent 会话"
>
<div className="launcher-agent-session-actions">
<button
type="button"
aria-label="新建 Agent 会话"
title="新建会话"
disabled={
agentChatBusy ||
agentChatBackgroundBusy ||
agentChatLegacySessionMode ||
agentChatSessions.length === 0 ||
!currentAgentChatAgent ||
currentAgentChatSessionMutationBlocked
}
onClick={() => void handleAgentChatCreateSession()}
>
<Plus size={16} aria-hidden="true" />
</button>
<button
type="button"
aria-label="分叉当前 Agent 会话"
title="从当前历史分叉会话"
disabled={
agentChatBusy ||
agentChatBackgroundBusy ||
agentChatLegacySessionMode ||
currentAgentChatSessionMutationBlocked ||
!currentAgentChatSession
}
onClick={() => void handleAgentChatForkSession()}
>
<GitFork size={16} aria-hidden="true" />
</button>
<button
type="button"
aria-label="归档当前 Agent 会话"
title="归档当前会话"
disabled={
agentChatBusy ||
agentChatBackgroundBusy ||
agentChatLegacySessionMode ||
currentAgentChatSessionMutationBlocked ||
!currentAgentChatSession ||
currentAgentChatSession.legacy ||
currentAgentChatSessionArchived
}
onClick={() => void handleAgentChatArchiveSession()}
>
<Archive size={16} aria-hidden="true" />
</button>
</div>
<div className="launcher-agent-session-list">
{currentAgentChatActiveSessions.map((session) => (
<button
key={session.sessionId}
type="button"
className={
session.sessionId === agentChatSelectedSessionId
? 'launcher-agent-session-active'
: ''
}
disabled={
agentChatBusy ||
agentChatBackgroundBusy ||
currentAgentChatSessionMutationBlocked
}
onClick={() => void handleAgentChatSelectSession(session)}
>
<span>{session.title}</span>
<small>
{formatAgentConversationSessionMeta(
session,
agentChatSessions,
agentChatActiveSessionId,
)}
</small>
</button>
))}
{currentAgentChatArchivedSessions.map((session) => (
<button
key={session.sessionId}
type="button"
className={`launcher-agent-session-archived${
session.sessionId === agentChatSelectedSessionId
? ' launcher-agent-session-active'
: ''
}`}
disabled={agentChatBusy || agentChatBackgroundBusy}
onClick={() => void handleAgentChatSelectSession(session)}
>
<span>{session.title}</span>
<small>{`已归档 · ${formatAgentConversationSessionMeta(
session,
agentChatSessions,
agentChatActiveSessionId,
)}`}</small>
</button>
))}
</div>
<small className="launcher-agent-session-status">
{agentChatSessionStatus}
</small>
</section>
<section className="launcher-agent-runtime-stack">
{currentAgentChatLlmWarning ? (
<div className="agent-llm-warning" role="status">
<strong>{currentAgentChatLlmWarning}</strong>
<button type="button" onClick={() => onRuntimeConfigOpen()}>
打开配置
</button>
</div>
) : null}
<AgentGoalStatusPanel
goal={agentChatGoal}
runtime={agentChatRuntime}
error={agentChatGoalError}
controlBusy={agentChatBusy || agentChatBackgroundBusy}
readOnly={currentAgentChatSessionArchived}
onStart={
agentChatSelectedSessionId && !agentChatGoalError
? () => openAgentChatGoalDialog('create')
: undefined
}
onEdit={
agentChatSelectedSessionId
? () => openAgentChatGoalDialog('edit')
: undefined
}
onPause={
currentAgentChatSessionArchived
? undefined
: () => void handleAgentChatGoalControl('pause')
}
onResume={
currentAgentChatSessionArchived
? undefined
: () => void handleAgentChatGoalControl('resume')
}
onClear={
currentAgentChatSessionArchived
? undefined
: () => void handleAgentChatGoalControl('clear')
}
/>
<AgentRuntimeStatusPanel
runtime={agentChatRuntime}
error={agentChatRuntimeError}
controlBusy={agentChatBusy || agentChatBackgroundBusy}
onCancelRuntimeTask={
currentAgentChatSessionArchived
? undefined
: (runId) => void handleAgentChatCancelRuntimeTask(runId)
}
onRetryRuntimeTask={
currentAgentChatSessionArchived
? undefined
: (runId) => void handleAgentChatRetryRuntimeTask(runId)
}
onConfirmRuntimeTask={
currentAgentChatSessionArchived
? undefined
: (runId, actionId) =>
void handleAgentChatConfirmRuntimeTask(runId, actionId)
}
onRejectRuntimeTask={
currentAgentChatSessionArchived
? undefined
: (runId, actionId) =>
void handleAgentChatRejectRuntimeTask(runId, actionId)
}
onSubmitUserInput={
currentAgentChatSessionArchived
? undefined
: handleAgentChatSubmitUserInput
}
onCompactContext={
currentAgentChatSessionArchived
? undefined
: () => void handleAgentChatCompactContext()
}
onRefreshRuntime={() =>
void loadAgentChatConversation(
agentChatSelectedAgentId,
agentChatProjectPath,
agentChatSelectedSessionId,
)
}
/>
</section>
<div className="launcher-agent-resume-slot">
{agentChatResumeConfirmation ? (
<div className="pending-command" role="status">
<span>
agent.resume
<small>{agentChatResumeConfirmation.detail}</small>
</span>
<button type="button" onClick={cancelAgentChatRuntimeResume}>
取消
</button>
<button
type="button"
onClick={() => void confirmAgentChatRuntimeResume()}
>
确认
</button>
</div>
) : null}
</div>
<div
ref={agentChatMessagesRef}
className="launcher-agent-chat-messages"
role="log"
aria-label="Agent 聊天记录"
aria-busy={currentAgentChatWaiting}
onScroll={handleAgentChatMessagesScroll}
tabIndex={0}
>
{agentChatMessages.length > 0 ? (
agentChatMessages.map((message, index) => (
<p
key={`${message.updatedAt}-${index}`}
className={`message message--${message.role}`}
>
{message.content}
</p>
))
) : (
<p className="status-line">暂无对话</p>
)}
{currentAgentChatWaiting ? (
<div
className="launcher-agent-chat-waiting"
role="status"
aria-live="polite"
aria-atomic="true"
>
<span aria-hidden="true" />
<div>
<strong>{currentAgentChatWaitingStatus}</strong>
<small>{currentAgentChatWaitingDetail}</small>
</div>
</div>
) : null}
</div>
<form
className="launcher-agent-chat-composer"
onSubmit={(event) => {
if (agentChatInteractionMode === 'run') {
event.preventDefault();
void handleAgentChatStartBackgroundTask();
return;
}
if (agentChatInteractionMode === 'goal') {
event.preventDefault();
openAgentChatGoalDialog(currentAgentChatGoalDialogMode);
return;
}
void handleAgentChatSubmit(event);
}}
>
<div
className="launcher-agent-chat-mode"
role="group"
aria-label="Agent 交互模式"
>
<button
type="button"
aria-pressed={agentChatInteractionMode === 'run'}
className={
agentChatInteractionMode === 'run' ? 'is-active' : ''
}
disabled={
agentChatBusy ||
agentChatBackgroundBusy ||
currentAgentChatNeedsUserInput ||
currentAgentChatSessionArchived
}
onClick={() => setAgentChatInteractionMode('run')}
>
执行
</button>
<button
type="button"
aria-pressed={agentChatInteractionMode === 'chat'}
className={
agentChatInteractionMode === 'chat' ? 'is-active' : ''
}
disabled={
agentChatBusy ||
agentChatBackgroundBusy ||
currentAgentChatNeedsUserInput ||
currentAgentChatSessionArchived
}
onClick={() => setAgentChatInteractionMode('chat')}
>
聊天
</button>
<button
type="button"
aria-pressed={agentChatInteractionMode === 'goal'}
className={
agentChatInteractionMode === 'goal' ? 'is-active' : ''
}
disabled={
agentChatBusy ||
agentChatBackgroundBusy ||
currentAgentChatNeedsUserInput ||
currentAgentChatSessionArchived
}
onClick={() => setAgentChatInteractionMode('goal')}
>
目标
</button>
</div>
{agentChatInteractionMode === 'goal' ? (
<div
className="launcher-agent-goal-composer-summary"
aria-label="当前持久目标"
>
{agentChatGoal
? `${currentAgentChatGoalStatus ?? '-'} · Revision ${
agentChatGoal.revision
} · ${agentChatGoal.outcome}`
: '尚未开始'}
</div>
) : (
<input
aria-label="Agent 聊天内容"
disabled={
agentChatBusy ||
agentChatBackgroundBusy ||
currentAgentChatNeedsUserInput ||
currentAgentChatLlmWarning !== null ||
currentAgentChatSessionArchived
}
value={agentChatInput}
onChange={(event) =>
setAgentChatInput(event.currentTarget.value)
}
/>
)}
{agentChatInteractionMode === 'run' &&
currentAgentChatSteerRuntime ? (
<select
aria-label="后台任务提交方式"
disabled={
agentChatBusy ||
agentChatBackgroundBusy ||
currentAgentChatNeedsUserInput
}
value={agentChatRunSubmitMode}
onChange={(event) =>
setAgentChatRunSubmitMode(
event.currentTarget.value as AgentBackgroundSubmitMode,
)
}
>
<option value="steer">追加当前 Run</option>
<option value="queue">排队新任务</option>
</select>
) : null}
<button
type="submit"
disabled={
agentChatBusy ||
agentChatBackgroundBusy ||
currentAgentChatNeedsUserInput ||
currentAgentChatLlmWarning !== null ||
currentAgentChatSessionArchived ||
(agentChatInteractionMode === 'goal' &&
(!agentChatSelectedSessionId ||
Boolean(agentChatGoalError) ||
(currentAgentChatGoalDialogMode === 'edit' &&
!['active', 'pause-requested', 'paused'].includes(
currentAgentChatGoalStatus ?? '',
))))
}
>
{agentChatInteractionMode === 'goal'
? currentAgentChatGoalDialogMode === 'create'
? '开始目标'
: '编辑目标'
: agentChatInteractionMode === 'run' &&
currentAgentChatSteerRuntime
? agentChatRunSubmitMode === 'queue'
? '排队任务'
: '追加指令'
: '发送'}
</button>
</form>
</section>
</section>
</section>
);
}
export function DeveloperAgentDialogs({
controller,
}: {
controller: DeveloperAgentPanelController;
}) {
const {
agentChatBackgroundBusy,
agentChatGoalDialog,
agentChatGoalDialogError,
closeAgentChatGoalDialog,
handleAgentChatGoalDialogSubmit,
setAgentChatGoalDialog,
} = controller;
return (
<>
{agentChatGoalDialog ? (
<div
className="launcher-dialog-backdrop"
role="presentation"
onMouseDown={(event) => {
if (event.target === event.currentTarget) {
closeAgentChatGoalDialog();
}
}}
>
<form
aria-labelledby="launcher-agent-goal-dialog-title"
aria-modal="true"
className="launcher-dialog launcher-agent-goal-dialog"
role="dialog"
onSubmit={(event) => void handleAgentChatGoalDialogSubmit(event)}
onKeyDown={(event) =>
closeDialogOnEscape(event, closeAgentChatGoalDialog)
}
>
<h2 id="launcher-agent-goal-dialog-title">
{agentChatGoalDialog.mode === 'create'
? '创建持久目标'
: '编辑持久目标'}
</h2>
<div className="launcher-agent-goal-fields">
<label>
Outcome
<textarea
aria-label="Goal outcome"
autoFocus
disabled={agentChatBackgroundBusy}
maxLength={4_000}
rows={5}
value={agentChatGoalDialog.outcome}
onChange={(event) => {
const outcome = event.currentTarget.value;
setAgentChatGoalDialog((current) =>
current ? { ...current, outcome } : current,
);
}}
/>
</label>
<label>
Constraints
<textarea
aria-label="Goal constraints"
disabled={agentChatBackgroundBusy}
rows={4}
value={agentChatGoalDialog.constraintsText}
onChange={(event) => {
const constraintsText = event.currentTarget.value;
setAgentChatGoalDialog((current) =>
current
? {
...current,
constraintsText,
}
: current,
);
}}
/>
</label>
<label>
Verification
<textarea
aria-label="Goal verification"
disabled={agentChatBackgroundBusy}
rows={4}
value={agentChatGoalDialog.verificationText}
onChange={(event) => {
const verificationText = event.currentTarget.value;
setAgentChatGoalDialog((current) =>
current
? {
...current,
verificationText,
}
: current,
);
}}
/>
</label>
</div>
{agentChatGoalDialogError ? (
<p className="launcher-agent-goal-dialog-error" role="alert">
{agentChatGoalDialogError}
</p>
) : null}
<div className="launcher-dialog-actions">
<button
type="button"
disabled={agentChatBackgroundBusy}
onClick={closeAgentChatGoalDialog}
>
取消
</button>
<button type="submit" disabled={agentChatBackgroundBusy}>
{agentChatGoalDialog.mode === 'create' ? '开始' : '保存'}
</button>
</div>
</form>
</div>
) : null}
</>
);
}