Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 38ad2ce256 | |||
| 65f8e44a88 |
@@ -174,6 +174,14 @@ _Avoid_: mock 先行堆积、前后端各自发散、先做排行榜 UI
|
|||||||
|
|
||||||
## 项目开发对话(DirectProject)
|
## 项目开发对话(DirectProject)
|
||||||
|
|
||||||
|
**DirectProject 专属聊天模块**:
|
||||||
|
AGC 普通项目聊天的独立容器,拥有 DirectProject 的聊天状态、运行态订阅、历史读取、发送队列、附件和中止交互,并把聊天投影交给专属表现层渲染;它不承接 Supervisor、Design Agent 或 Planning V2 的运行态。
|
||||||
|
_Avoid_: 把 DirectProject 作为项目总控聊天的一个布尔分支、把四种 Agent 会话抽象成同一事实源
|
||||||
|
|
||||||
|
**项目工作台布局**:
|
||||||
|
承载本地项目的资源工作区、项目级工具和独立聊天产品路径的外层界面;布局拥有跨面板的账户/钱包入口,聊天模块只负责项目对话,不嵌套账户展示。
|
||||||
|
_Avoid_: 把钱包入口塞进聊天设置、让聊天组件拥有工作台级账户状态
|
||||||
|
|
||||||
**项目对话历史**:
|
**项目对话历史**:
|
||||||
AGC 本地项目内 Codex 原始对话条目的持久集合,是聊天展示、工具卡片和线程恢复注入的唯一持久事实源。
|
AGC 本地项目内 Codex 原始对话条目的持久集合,是聊天展示、工具卡片和线程恢复注入的唯一持久事实源。
|
||||||
_Avoid_: 会话缓存、展示态历史、按 UI 需要另存的对话副本
|
_Avoid_: 会话缓存、展示态历史、按 UI 需要另存的对话副本
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -2,7 +2,7 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
|
|||||||
import { $getNodeByKey, type NodeKey } from 'lexical';
|
import { $getNodeByKey, type NodeKey } from 'lexical';
|
||||||
import { Paperclip, X } from 'lucide-react';
|
import { Paperclip, X } from 'lucide-react';
|
||||||
|
|
||||||
import type { DirectCodexUserAttachmentReferencePart } from './generated';
|
import type { DirectCodexUserAttachmentReferencePart } from '../../view/project-development/chat/generated/DirectCodexUserAttachmentReferencePart';
|
||||||
|
|
||||||
export function AttachmentReferenceChip({
|
export function AttachmentReferenceChip({
|
||||||
attachment,
|
attachment,
|
||||||
|
|||||||
+1
-1
@@ -9,8 +9,8 @@ import {
|
|||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import type { DirectCodexUserAttachmentReferencePart } from '../../view/project-development/chat/generated/DirectCodexUserAttachmentReferencePart';
|
||||||
import { AttachmentReferenceChip } from './AttachmentReferenceChip';
|
import { AttachmentReferenceChip } from './AttachmentReferenceChip';
|
||||||
import type { DirectCodexUserAttachmentReferencePart } from './generated';
|
|
||||||
|
|
||||||
export type SerializedAttachmentReferenceNode = Spread<
|
export type SerializedAttachmentReferenceNode = Spread<
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-17
@@ -1,5 +1,4 @@
|
|||||||
import { Settings2, ShieldCheck, Wallet, X } from 'lucide-react';
|
import { Settings2, ShieldCheck, X } from 'lucide-react';
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -17,19 +16,16 @@ import { RuntimeConfigDialog } from '../runtime-config/RuntimeConfigDialog';
|
|||||||
* 组成:
|
* 组成:
|
||||||
* - 「运行配置」打开既有 `RuntimeConfigDialog`(独立浮层,自己有 backdrop);
|
* - 「运行配置」打开既有 `RuntimeConfigDialog`(独立浮层,自己有 backdrop);
|
||||||
* - 「操作权限」打开由 `ApprovalModeDialog` 提供的选择面板;
|
* - 「操作权限」打开由 `ApprovalModeDialog` 提供的选择面板;
|
||||||
* - 「泥点」直接渲染父级传入的 `walletEntry`(为空则整行不渲染)。
|
|
||||||
*/
|
*/
|
||||||
export function ProjectSupervisorSettingsDialog({
|
export function ProjectSupervisorSettingsDialog({
|
||||||
projectPath,
|
projectPath,
|
||||||
currentApprovalLabel,
|
currentApprovalLabel,
|
||||||
onOpenApproval,
|
onOpenApproval,
|
||||||
walletEntry,
|
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
projectPath: string;
|
projectPath: string;
|
||||||
currentApprovalLabel: string;
|
currentApprovalLabel: string;
|
||||||
onOpenApproval: () => void;
|
onOpenApproval: () => void;
|
||||||
walletEntry?: ReactNode;
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [runtimeConfigOpen, setRuntimeConfigOpen] = useState(false);
|
const [runtimeConfigOpen, setRuntimeConfigOpen] = useState(false);
|
||||||
@@ -90,18 +86,6 @@ export function ProjectSupervisorSettingsDialog({
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{walletEntry ? (
|
|
||||||
<div className="project-supervisor-settings-row is-static">
|
|
||||||
<span className="project-supervisor-settings-row-main">
|
|
||||||
<Wallet size={16} aria-hidden="true" />
|
|
||||||
<span>
|
|
||||||
<strong>泥点</strong>
|
|
||||||
<small>当前余额与充值入口</small>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<div className="game-workbench-chat-wallet">{walletEntry}</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+90
-474
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -61,6 +61,7 @@ import {
|
|||||||
createProjectResourcePreviewRequestId,
|
createProjectResourcePreviewRequestId,
|
||||||
createProjectResourcePreviewScopeId,
|
createProjectResourcePreviewScopeId,
|
||||||
} from '../../services/projectResourcePreviewTransport';
|
} from '../../services/projectResourcePreviewTransport';
|
||||||
|
import type { DirectCodexUserContentPart } from '../../view/project-development/chat/generated/DirectCodexUserContentPart';
|
||||||
import {
|
import {
|
||||||
$createAttachmentReferenceNode,
|
$createAttachmentReferenceNode,
|
||||||
$isAttachmentReferenceNode,
|
$isAttachmentReferenceNode,
|
||||||
@@ -73,7 +74,6 @@ import {
|
|||||||
writeChatPromptPolishReminderDisabled,
|
writeChatPromptPolishReminderDisabled,
|
||||||
} from './chatPromptPolish';
|
} from './chatPromptPolish';
|
||||||
import { ChatPromptPolishReminder } from './ChatPromptPolishReminder';
|
import { ChatPromptPolishReminder } from './ChatPromptPolishReminder';
|
||||||
import type { DirectCodexUserContentPart } from './generated';
|
|
||||||
import {
|
import {
|
||||||
$createResourceReferenceNode,
|
$createResourceReferenceNode,
|
||||||
$isResourceReferenceNode,
|
$isResourceReferenceNode,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* 依次发出。队列项能在输入盒上方单独取消。这里只放与 React 无关的纯逻辑,便于单测。
|
* 依次发出。队列项能在输入盒上方单独取消。这里只放与 React 无关的纯逻辑,便于单测。
|
||||||
*/
|
*/
|
||||||
import type { GameCreationAppAssetManifestEntry } from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
import type { GameCreationAppAssetManifestEntry } from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||||
import type { DirectCodexUserItem } from './generated';
|
import type { DirectCodexUserItem } from '../../view/project-development/chat/generated/DirectCodexUserItem';
|
||||||
import { directCodexContentToPromptText } from './resourceReferences';
|
import { directCodexContentToPromptText } from './resourceReferences';
|
||||||
|
|
||||||
/** 队列上限:满了以后拒绝入队并给出可读提示,而不是静默丢消息。 */
|
/** 队列上限:满了以后拒绝入队并给出可读提示,而不是静默丢消息。 */
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { resolveTauriInvoke } from '../../app/tauri';
|
import { resolveTauriInvoke } from '../../app/tauri';
|
||||||
import type { DirectCodexUserContentPart } from './generated';
|
import type { DirectCodexUserContentPart } from '../../view/project-development/chat/generated/DirectCodexUserContentPart';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 「不再提醒」偏好存本机 localStorage,不进 manifest、不进后端。
|
* 「不再提醒」偏好存本机 localStorage,不进 manifest、不进后端。
|
||||||
|
|||||||
@@ -5,13 +5,11 @@
|
|||||||
* 入口转发:前端不再自己抄一份形状,字段增删必须改 Rust。
|
* 入口转发:前端不再自己抄一份形状,字段增删必须改 Rust。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type {
|
export type { DirectThreadConsumeResult } from '../../view/project-development/chat/generated/DirectThreadConsumeResult';
|
||||||
DirectThreadConsumeResult,
|
export type { DirectThreadDeltaKind } from '../../view/project-development/chat/generated/DirectThreadDeltaKind';
|
||||||
DirectThreadDeltaKind,
|
export type { DirectThreadEvent } from '../../view/project-development/chat/generated/DirectThreadEvent';
|
||||||
DirectThreadEvent,
|
export type { DirectThreadFileChange } from '../../view/project-development/chat/generated/DirectThreadFileChange';
|
||||||
DirectThreadFileChange,
|
export type { DirectThreadHistorySlice } from '../../view/project-development/chat/generated/DirectThreadHistorySlice';
|
||||||
DirectThreadHistorySlice,
|
export type { DirectThreadItem } from '../../view/project-development/chat/generated/DirectThreadItem';
|
||||||
DirectThreadItem,
|
export type { DirectThreadRequestKind } from '../../view/project-development/chat/generated/DirectThreadRequestKind';
|
||||||
DirectThreadRequestKind,
|
export type { DirectThreadSubscriptionBootstrap } from '../../view/project-development/chat/generated/DirectThreadSubscriptionBootstrap';
|
||||||
DirectThreadSubscriptionBootstrap,
|
|
||||||
} from './generated';
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
export type { DirectCodexUserContentPart } from './DirectCodexUserContentPart';
|
|
||||||
export type { DirectCodexUserAttachmentReferencePart } from './DirectCodexUserAttachmentReferencePart';
|
|
||||||
export type { DirectCodexUserItem } from './DirectCodexUserItem';
|
|
||||||
export type { DirectCodexUserMessageItem } from './DirectCodexUserMessageItem';
|
|
||||||
export type { DirectCodexUserRole } from './DirectCodexUserRole';
|
|
||||||
export type { DirectCodexUserRuntimeRegionPart } from './DirectCodexUserRuntimeRegionPart';
|
|
||||||
export type { DirectCodexUserMessageEnvelope } from './DirectCodexUserMessageEnvelope';
|
|
||||||
export type { DirectThreadConsumeResult } from './DirectThreadConsumeResult';
|
|
||||||
export type { DirectThreadDeltaKind } from './DirectThreadDeltaKind';
|
|
||||||
export type { DirectThreadEvent } from './DirectThreadEvent';
|
|
||||||
export type { DirectThreadFileChange } from './DirectThreadFileChange';
|
|
||||||
export type { DirectThreadHistorySlice } from './DirectThreadHistorySlice';
|
|
||||||
export type { DirectThreadItem } from './DirectThreadItem';
|
|
||||||
export type { DirectThreadRequestKind } from './DirectThreadRequestKind';
|
|
||||||
export type { DirectThreadSubscriptionBootstrap } from './DirectThreadSubscriptionBootstrap';
|
|
||||||
@@ -10,10 +10,8 @@ import {
|
|||||||
buildGameCreationAppAssetTagLibrary,
|
buildGameCreationAppAssetTagLibrary,
|
||||||
type GameCreationAppAssetTagLibraryEntry,
|
type GameCreationAppAssetTagLibraryEntry,
|
||||||
} from '../../../../../packages/shared/src/contracts/gameCreationAppAssetTagLibrary';
|
} from '../../../../../packages/shared/src/contracts/gameCreationAppAssetTagLibrary';
|
||||||
import type {
|
import type { DirectCodexUserContentPart } from '../../view/project-development/chat/generated/DirectCodexUserContentPart';
|
||||||
DirectCodexUserContentPart,
|
import type { DirectCodexUserItem } from '../../view/project-development/chat/generated/DirectCodexUserItem';
|
||||||
DirectCodexUserItem,
|
|
||||||
} from './generated';
|
|
||||||
|
|
||||||
export type ResourceReferenceSource =
|
export type ResourceReferenceSource =
|
||||||
| 'asset-picker'
|
| 'asset-picker'
|
||||||
|
|||||||
@@ -5850,6 +5850,17 @@ iframe.preview-frame {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.game-workbench-layout-account {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 12px;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-workbench-layout-account .launcher-account-bar {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
.game-workbench-layout.is-ui-editor {
|
.game-workbench-layout.is-ui-editor {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|||||||
+453
@@ -0,0 +1,453 @@
|
|||||||
|
import { ArrowUp, AtSign, Loader2, Settings } from 'lucide-react';
|
||||||
|
import type { FormEventHandler, RefObject, UIEventHandler } from 'react';
|
||||||
|
import { Fragment, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AgentMessageContent,
|
||||||
|
type AgentMessageTone,
|
||||||
|
} from '../../../../../../packages/shared/src/components/AgentMessageContent';
|
||||||
|
import type { ChatMessage } from '../../../app/types';
|
||||||
|
import { ChatMarkdownMessage } from '../../../components/ChatMarkdownMessage';
|
||||||
|
import { projectWorkspaceStatusForDisplay } from '../../../features/agent-runtime';
|
||||||
|
import type { DirectCodexTurnAttachment } from '../../../features/app-shell/directCodexTurnAttachments';
|
||||||
|
import type { QueuedChatTurn } from '../../../features/project-workspace/chatComposerQueue';
|
||||||
|
import {
|
||||||
|
ComposerAttachmentMenu,
|
||||||
|
ComposerPendingAttachments,
|
||||||
|
ComposerReasoningEffortSelect,
|
||||||
|
ComposerStopButton,
|
||||||
|
ComposerTurnQueue,
|
||||||
|
ComposerVoiceButton,
|
||||||
|
} from '../../../features/project-workspace/ComposerControls';
|
||||||
|
import {
|
||||||
|
ConversationModelSelect,
|
||||||
|
type ConversationModelSelectHandle,
|
||||||
|
} from '../../../features/project-workspace/ConversationModelSelect';
|
||||||
|
import type { DirectChatEntry } from '../../../features/project-workspace/directThreadChat';
|
||||||
|
import {
|
||||||
|
buildDirectChatTurns,
|
||||||
|
type DirectChatBlock,
|
||||||
|
type DirectChatTurn,
|
||||||
|
directMessageTimestamp,
|
||||||
|
} from '../../../features/project-workspace/directTurnPresentation';
|
||||||
|
import { ProjectSupervisorSettingsDialog } from '../../../features/project-workspace/ProjectSupervisorSettingsDialog';
|
||||||
|
import {
|
||||||
|
ResourceReferenceInput,
|
||||||
|
type ResourceReferenceInputHandle,
|
||||||
|
} from '../../../features/project-workspace/ResourceReferenceInput';
|
||||||
|
import type {
|
||||||
|
ChatComposerDraft,
|
||||||
|
ChatReference,
|
||||||
|
} from '../../../features/project-workspace/resourceReferences';
|
||||||
|
import { ToolCallGroup } from '../../../features/project-workspace/ToolCallGroup';
|
||||||
|
import {
|
||||||
|
formatClockTime,
|
||||||
|
formatTurnDuration,
|
||||||
|
} from '../../../features/project-workspace/toolCallGroupPresentation';
|
||||||
|
import { type ApprovalMode, approvalModeLabel } from '../approvalMode';
|
||||||
|
import { ApprovalModeDialog } from '../ApprovalModeDialog';
|
||||||
|
|
||||||
|
type AssetManifestEntry =
|
||||||
|
import('../../../../../../packages/shared/src/contracts/gameCreationApp').GameCreationAppAssetManifestEntry;
|
||||||
|
type GameIterationVersion =
|
||||||
|
import('../../../../../../packages/shared/src/contracts/gameCreationApp').GameIterationVersion;
|
||||||
|
|
||||||
|
export type DirectProjectChatViewProps = {
|
||||||
|
activeVersionId?: string | null;
|
||||||
|
attachments: DirectCodexTurnAttachment[];
|
||||||
|
attachmentNotice: string;
|
||||||
|
chatInput: string;
|
||||||
|
chatReferences: ChatReference[];
|
||||||
|
chatProjectAssets: AssetManifestEntry[];
|
||||||
|
composerRef: RefObject<ResourceReferenceInputHandle | null>;
|
||||||
|
composerNotice: string;
|
||||||
|
conversationMessages: ChatMessage[];
|
||||||
|
directEntries: DirectChatEntry[];
|
||||||
|
directTurnRunning: boolean;
|
||||||
|
hiddenConversationCount: number;
|
||||||
|
hasEarlierConversationMessages: boolean;
|
||||||
|
messagesRef: RefObject<HTMLDivElement | null>;
|
||||||
|
onCancelQueuedTurn: (id: string) => void;
|
||||||
|
onCancelTurn: () => void;
|
||||||
|
onChatInputChange: (draft: ChatComposerDraft) => void;
|
||||||
|
onRemoveAttachment: (index: number) => void;
|
||||||
|
onScroll: UIEventHandler<HTMLDivElement>;
|
||||||
|
onShowEarlierMessages: () => void;
|
||||||
|
onSubmit: FormEventHandler<HTMLFormElement>;
|
||||||
|
onUploadFiles: (files: readonly File[]) => void;
|
||||||
|
projectPath: string;
|
||||||
|
queuedTurns: QueuedChatTurn[];
|
||||||
|
turnCancelling: boolean;
|
||||||
|
versions?: GameIterationVersion[];
|
||||||
|
workspaceStatus: string;
|
||||||
|
controlBusy: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DirectProject 的独立表现容器。
|
||||||
|
*
|
||||||
|
* 这个组件只处理 DirectProject 的输入框、回合分区、运行中状态和设置表现;
|
||||||
|
* Supervisor / Design Agent / Planning V2 的运行态不在这里出现。
|
||||||
|
*/
|
||||||
|
export function DirectProjectChatView({
|
||||||
|
activeVersionId = null,
|
||||||
|
attachments,
|
||||||
|
attachmentNotice,
|
||||||
|
chatInput,
|
||||||
|
chatReferences,
|
||||||
|
chatProjectAssets,
|
||||||
|
composerRef,
|
||||||
|
composerNotice,
|
||||||
|
conversationMessages,
|
||||||
|
directEntries,
|
||||||
|
directTurnRunning,
|
||||||
|
hiddenConversationCount,
|
||||||
|
hasEarlierConversationMessages,
|
||||||
|
messagesRef,
|
||||||
|
onCancelQueuedTurn,
|
||||||
|
onCancelTurn,
|
||||||
|
onChatInputChange,
|
||||||
|
onRemoveAttachment,
|
||||||
|
onScroll,
|
||||||
|
onShowEarlierMessages,
|
||||||
|
onSubmit,
|
||||||
|
onUploadFiles,
|
||||||
|
projectPath,
|
||||||
|
queuedTurns,
|
||||||
|
turnCancelling,
|
||||||
|
versions,
|
||||||
|
workspaceStatus,
|
||||||
|
controlBusy,
|
||||||
|
}: DirectProjectChatViewProps) {
|
||||||
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
|
const [turnUsageNow, setTurnUsageNow] = useState(() => Date.now());
|
||||||
|
const [voiceNotice, setVoiceNotice] = useState('');
|
||||||
|
const [approvalOpen, setApprovalOpen] = useState(false);
|
||||||
|
const [approvalMode, setApprovalMode] = useState<ApprovalMode>('strict');
|
||||||
|
const [approvalNotice, setApprovalNotice] = useState('');
|
||||||
|
const [modelReady, setModelReady] = useState(false);
|
||||||
|
const [modelValidating, setModelValidating] = useState(false);
|
||||||
|
const modelSelectRef = useRef<ConversationModelSelectHandle>(null);
|
||||||
|
const modelValidateInFlightRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!directTurnRunning) return;
|
||||||
|
setTurnUsageNow(Date.now());
|
||||||
|
const timer = setInterval(() => setTurnUsageNow(Date.now()), 1000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [directTurnRunning]);
|
||||||
|
|
||||||
|
const directTurns = buildDirectChatTurns({
|
||||||
|
entries: directEntries,
|
||||||
|
localMessages: conversationMessages,
|
||||||
|
turnRunning: directTurnRunning,
|
||||||
|
});
|
||||||
|
const activeTurnStartedAt =
|
||||||
|
directTurns.find((turn) => turn.active)?.startedAt ?? 0;
|
||||||
|
|
||||||
|
const renderTurnUsage = (turn: DirectChatTurn) => {
|
||||||
|
if (turn.active || !turn.startedAt) return null;
|
||||||
|
const endedAt = Math.max(turn.endedAt, turn.startedAt);
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
className="message-turn-usage"
|
||||||
|
data-testid="turn-usage"
|
||||||
|
data-turn-running="false"
|
||||||
|
>
|
||||||
|
{`本轮结束于 ${new Date(endedAt).toLocaleTimeString('zh-CN', {
|
||||||
|
hour12: false,
|
||||||
|
})} · 耗时 ${formatTurnDuration(endedAt - turn.startedAt) ?? '0秒'}`}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitLabel = controlBusy ? '思考中' : '发送';
|
||||||
|
const submitButton = (
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="project-supervisor-submit-button"
|
||||||
|
aria-label={submitLabel}
|
||||||
|
title={submitLabel}
|
||||||
|
disabled={controlBusy || !modelReady || modelValidating}
|
||||||
|
>
|
||||||
|
{controlBusy ? (
|
||||||
|
<Loader2 size={16} aria-hidden="true" className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ArrowUp size={16} aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderBlock = (
|
||||||
|
turn: DirectChatTurn,
|
||||||
|
block: DirectChatBlock,
|
||||||
|
tone: AgentMessageTone = 'body',
|
||||||
|
streamingKey: string | null,
|
||||||
|
) => {
|
||||||
|
if (block.kind === 'tools') {
|
||||||
|
return (
|
||||||
|
<ToolCallGroup
|
||||||
|
key={block.key}
|
||||||
|
calls={block.calls}
|
||||||
|
userSentAt={turn.startedAt}
|
||||||
|
active={turn.active}
|
||||||
|
className="message-tool-call"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (block.kind === 'reasoning') {
|
||||||
|
return (
|
||||||
|
<AgentMessageContent
|
||||||
|
key={block.key}
|
||||||
|
as="details"
|
||||||
|
tone="process"
|
||||||
|
className="design-agent-reasoning"
|
||||||
|
aria-label="思考过程"
|
||||||
|
>
|
||||||
|
<summary>思考过程</summary>
|
||||||
|
<pre>{block.text}</pre>
|
||||||
|
</AgentMessageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const role = block.kind === 'user' ? 'user' : 'assistant';
|
||||||
|
return (
|
||||||
|
<div key={block.key} className={`message message--${role}`}>
|
||||||
|
<AgentMessageContent tone={tone}>
|
||||||
|
<ChatMarkdownMessage
|
||||||
|
role={role}
|
||||||
|
text={block.text}
|
||||||
|
streaming={block.key === streamingKey}
|
||||||
|
/>
|
||||||
|
</AgentMessageContent>
|
||||||
|
{block.kind === 'user' && block.at > 0 ? (
|
||||||
|
<time
|
||||||
|
className="message-sent-at"
|
||||||
|
dateTime={new Date(directMessageTimestamp(block.at)).toISOString()}
|
||||||
|
title={`发送于 ${new Date(block.at).toLocaleString('zh-CN', { hour12: false })}`}
|
||||||
|
>
|
||||||
|
{formatClockTime(block.at)}
|
||||||
|
</time>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="project-supervisor-surface is-direct-codex"
|
||||||
|
aria-label="陶泥儿项目对话"
|
||||||
|
>
|
||||||
|
<div className="project-supervisor-conversation">
|
||||||
|
<header className="project-supervisor-topbar">
|
||||||
|
<span className="project-supervisor-topbar-status" aria-live="polite">
|
||||||
|
<span
|
||||||
|
className={`project-supervisor-topbar-dot${controlBusy ? ' is-busy' : ''}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{controlBusy
|
||||||
|
? '陶泥儿正在处理'
|
||||||
|
: projectWorkspaceStatusForDisplay(workspaceStatus)}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="project-supervisor-settings-trigger"
|
||||||
|
aria-label="设置"
|
||||||
|
title="设置"
|
||||||
|
onClick={() => setSettingsOpen(true)}
|
||||||
|
>
|
||||||
|
<Settings size={16} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<div
|
||||||
|
ref={messagesRef}
|
||||||
|
className="message-list project-supervisor-message-list"
|
||||||
|
aria-label="陶泥儿消息"
|
||||||
|
onScroll={onScroll}
|
||||||
|
>
|
||||||
|
{hiddenConversationCount > 0 || hasEarlierConversationMessages ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="message-history-more"
|
||||||
|
onClick={onShowEarlierMessages}
|
||||||
|
>
|
||||||
|
{hiddenConversationCount > 0
|
||||||
|
? `显示更早 · 还有 ${hiddenConversationCount} 条对话`
|
||||||
|
: '显示更早的对话'}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
{directTurns.map((turn) => {
|
||||||
|
const streamingKey = turn.active
|
||||||
|
? ([...turn.process]
|
||||||
|
.reverse()
|
||||||
|
.find((block) => block.kind === 'assistant')?.key ?? null)
|
||||||
|
: null;
|
||||||
|
return (
|
||||||
|
<Fragment key={turn.key}>
|
||||||
|
{turn.users.map((block) =>
|
||||||
|
renderBlock(turn, block, 'body', streamingKey),
|
||||||
|
)}
|
||||||
|
{turn.process.length > 0 ? (
|
||||||
|
turn.active ? (
|
||||||
|
turn.process.map((block) =>
|
||||||
|
renderBlock(turn, block, 'process', streamingKey),
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<details
|
||||||
|
className="message-turn-process"
|
||||||
|
data-testid="turn-process"
|
||||||
|
>
|
||||||
|
<summary>执行过程</summary>
|
||||||
|
<div className="message-turn-process-body">
|
||||||
|
{turn.process.map((block) =>
|
||||||
|
renderBlock(turn, block, 'process', streamingKey),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
{turn.finals.map((block) =>
|
||||||
|
renderBlock(turn, block, 'body', streamingKey),
|
||||||
|
)}
|
||||||
|
{renderTurnUsage(turn)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{directTurnRunning ? (
|
||||||
|
<AgentMessageContent
|
||||||
|
as="section"
|
||||||
|
tone="process"
|
||||||
|
className="project-supervisor-process-card is-active"
|
||||||
|
aria-label="陶泥儿执行过程"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-atomic="false"
|
||||||
|
role="status"
|
||||||
|
data-runtime-owned="true"
|
||||||
|
>
|
||||||
|
<header>
|
||||||
|
<span aria-hidden="true" />
|
||||||
|
<strong>陶泥儿正在处理</strong>
|
||||||
|
{activeTurnStartedAt > 0 ? (
|
||||||
|
<em className="project-supervisor-process-elapsed">
|
||||||
|
{`已耗时 ${
|
||||||
|
formatTurnDuration(
|
||||||
|
Math.max(0, turnUsageNow - activeTurnStartedAt),
|
||||||
|
) ?? '0秒'
|
||||||
|
}`}
|
||||||
|
</em>
|
||||||
|
) : null}
|
||||||
|
</header>
|
||||||
|
</AgentMessageContent>
|
||||||
|
) : null}
|
||||||
|
<form
|
||||||
|
className="project-supervisor-composer is-direct-codex"
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (modelValidateInFlightRef.current) return;
|
||||||
|
modelValidateInFlightRef.current = true;
|
||||||
|
setModelValidating(true);
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const ready = modelSelectRef.current
|
||||||
|
? await modelSelectRef.current.ensureUsable()
|
||||||
|
: modelReady;
|
||||||
|
if (ready) onSubmit(event);
|
||||||
|
} finally {
|
||||||
|
modelValidateInFlightRef.current = false;
|
||||||
|
setModelValidating(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ComposerTurnQueue
|
||||||
|
turns={queuedTurns}
|
||||||
|
assets={chatProjectAssets}
|
||||||
|
onCancel={onCancelQueuedTurn}
|
||||||
|
/>
|
||||||
|
<ComposerPendingAttachments
|
||||||
|
attachments={attachments}
|
||||||
|
onRemove={onRemoveAttachment}
|
||||||
|
/>
|
||||||
|
<ResourceReferenceInput
|
||||||
|
ref={composerRef}
|
||||||
|
ariaLabel="陶泥儿对话内容"
|
||||||
|
activeVersionId={activeVersionId}
|
||||||
|
versions={versions}
|
||||||
|
assets={chatProjectAssets}
|
||||||
|
projectPath={projectPath}
|
||||||
|
disabled={modelValidating}
|
||||||
|
rows={3}
|
||||||
|
value={chatInput}
|
||||||
|
references={chatReferences}
|
||||||
|
placeholder="描述你的想法,或 @ 引用素材"
|
||||||
|
onChange={onChatInputChange}
|
||||||
|
/>
|
||||||
|
<div className="project-supervisor-composer-controls">
|
||||||
|
<div className="project-supervisor-composer-controls-left">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="project-supervisor-reference-trigger"
|
||||||
|
aria-label="插入素材引用"
|
||||||
|
title="插入素材引用"
|
||||||
|
disabled={controlBusy}
|
||||||
|
onClick={() => composerRef.current?.openPicker()}
|
||||||
|
>
|
||||||
|
<AtSign size={15} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<ComposerAttachmentMenu
|
||||||
|
disabled={controlBusy || modelValidating}
|
||||||
|
onPickFiles={onUploadFiles}
|
||||||
|
onOpenReferencePicker={() => composerRef.current?.openPicker()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="project-supervisor-composer-controls-right">
|
||||||
|
<ComposerReasoningEffortSelect disabled={controlBusy} />
|
||||||
|
<ConversationModelSelect
|
||||||
|
ref={modelSelectRef}
|
||||||
|
disabled={controlBusy}
|
||||||
|
onReady={setModelReady}
|
||||||
|
projectPath={projectPath}
|
||||||
|
/>
|
||||||
|
<ComposerVoiceButton
|
||||||
|
disabled={controlBusy || modelValidating}
|
||||||
|
onTranscript={(text) => composerRef.current?.insertText(text)}
|
||||||
|
onNotice={setVoiceNotice}
|
||||||
|
/>
|
||||||
|
{controlBusy ? (
|
||||||
|
<ComposerStopButton
|
||||||
|
cancelling={turnCancelling}
|
||||||
|
onCancel={onCancelTurn}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
submitButton
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{composerNotice || attachmentNotice || voiceNotice ? (
|
||||||
|
<p className="project-supervisor-composer-notice" role="status">
|
||||||
|
{composerNotice || attachmentNotice || voiceNotice}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{settingsOpen ? (
|
||||||
|
<ProjectSupervisorSettingsDialog
|
||||||
|
projectPath={projectPath}
|
||||||
|
currentApprovalLabel={approvalModeLabel(approvalMode)}
|
||||||
|
onOpenApproval={() => setApprovalOpen(true)}
|
||||||
|
onClose={() => setSettingsOpen(false)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{settingsOpen && approvalOpen ? (
|
||||||
|
<ApprovalModeDialog
|
||||||
|
approvalMode={approvalMode}
|
||||||
|
notice={approvalNotice}
|
||||||
|
onSelect={setApprovalMode}
|
||||||
|
onNotice={setApprovalNotice}
|
||||||
|
onClose={() => setApprovalOpen(false)}
|
||||||
|
closeOnEscape={false}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user