Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
# Chat Top Entry MVP PRD

Date: 2026-07-01
Status: Simplified for product/design review
Status: Archived historical concept; not a current implementation contract
Target surface: `apps/aevatar-console-web`
Related prototype: `../prototypes/2026-07-01-chat-top-entry-prototype.html`
Related PNG: `../prototypes/2026-07-01-chat-top-entry-prototype.png`

> **Superseded behavior:** The current Chat surface uses backend-backed history
> and typed runtime events. Assistant prose that asks the user to choose or
> confirm something remains a completed text turn, and the user continues
> through the composer. Do not infer `needs_confirmation` or `creating` from
> message text, and do not render a generic confirmation action from prose.
> Approval controls must come from an explicit typed approval or intervention
> event. The material below is retained only as the July 2026 design record.

## Product Thesis

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aevatar Chat MVP Prototype</title>
<title>Archived Aevatar Chat MVP Prototype</title>
<style>
:root {
--page-bg: #f5f7fb;
Expand Down Expand Up @@ -559,12 +559,18 @@
<main class="page">
<header class="header">
<div>
<h1>Aevatar Chat MVP Prototype</h1>
<h1>Archived Aevatar Chat MVP Prototype</h1>
<p>
Simplified v1: create a chat session, create Team / Member /
Workflow through conversation, and return to local history. Studio
is the only primary jump after creation.
</p>
<p role="note">
<strong>Historical concept only.</strong> The current Chat surface
does not derive confirmation actions from assistant prose. Users
answer clarification or selection requests in the composer; only
typed runtime approval events can render approval controls.
</p>
</div>
<span class="scope-chip">Chat + local history</span>
</header>
Expand Down
7 changes: 1 addition & 6 deletions apps/aevatar-console-web/src/locales/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ const enUSMessages = {
'pages.chat.index.chatStopped': 'Chat stopped.',
'pages.chat.index.composerPlaceholder':
'Describe the workflow you want, or ask about the current setup...',
'pages.chat.index.confirmAndCreate': 'Confirm and create',
'pages.chat.index.confirmPrompt': 'Confirm. Please create it now.',
'pages.chat.index.continuationContextMissing':
'The continuation may have been accepted, but its turn identity was not received. Reload this page before continuing.',
'pages.chat.index.cancel': 'Cancel',
Expand Down Expand Up @@ -80,15 +78,12 @@ const enUSMessages = {
'pages.chat.index.resolvingScope': 'Resolving scope',
'pages.chat.index.retry': 'Retry',
'pages.chat.index.retryHistory': 'Retry chat history',
'pages.chat.index.reviewPlan': 'Review the plan before creating resources.',
'pages.chat.index.scopeMismatch':
'Requested scope {requestedScopeId} does not match authenticated scope {authenticatedScopeId}. Open Chat from the active workspace or sign in again.',
'pages.chat.index.scopeValue': 'Scope {scopeId}',
'pages.chat.index.status.completed': 'Completed',
'pages.chat.index.status.creating': 'Creating',
'pages.chat.index.status.completed': 'Ready for reply',
'pages.chat.index.status.draft': 'Draft',
'pages.chat.index.status.error': 'Error',
'pages.chat.index.status.needsConfirmation': 'Needs confirmation',
'pages.chat.index.status.streaming': 'Streaming',
'pages.chat.index.status.studioReady': 'Studio ready',
'pages.chat.index.time.daysAgo': '{count}d ago',
Expand Down
7 changes: 1 addition & 6 deletions apps/aevatar-console-web/src/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ const zhCNMessages = {
'pages.gagents.index.platformBreadcrumb': '平台',
'pages.chat.index.chatStopped': 'Chat 已停止。',
'pages.chat.index.composerPlaceholder': '描述你想要的 workflow,或询问当前配置...',
'pages.chat.index.confirmAndCreate': '确认并创建',
'pages.chat.index.confirmPrompt': '确认,请现在创建。',
'pages.chat.index.continuationContextMissing':
'续聊请求可能已经被接受,但未收到本轮身份。请重新加载此页面后再继续。',
'pages.chat.index.cancel': '取消',
Expand Down Expand Up @@ -74,15 +72,12 @@ const zhCNMessages = {
'pages.chat.index.resolvingScope': '正在解析 scope',
'pages.chat.index.retry': '重试',
'pages.chat.index.retryHistory': '重新加载会话历史',
'pages.chat.index.reviewPlan': '创建资源前请先确认计划。',
'pages.chat.index.scopeMismatch':
'请求的 scope {requestedScopeId} 与当前认证 scope {authenticatedScopeId} 不一致。请从当前工作区重新打开 Chat,或重新登录。',
'pages.chat.index.scopeValue': 'Scope {scopeId}',
'pages.chat.index.status.completed': '已完成',
'pages.chat.index.status.creating': '创建中',
'pages.chat.index.status.completed': '可继续回复',
'pages.chat.index.status.draft': '草稿',
'pages.chat.index.status.error': '错误',
'pages.chat.index.status.needsConfirmation': '待确认',
'pages.chat.index.status.streaming': '生成中',
'pages.chat.index.status.studioReady': 'Studio 已就绪',
'pages.chat.index.time.daysAgo': '{count} 天前',
Expand Down
2 changes: 0 additions & 2 deletions apps/aevatar-console-web/src/pages/chat/chatTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ export type ChatStudioTarget = {
export type LocalChatStatus =
| "draft"
| "streaming"
| "needs_confirmation"
| "creating"
| "completed_text"
| "completed_with_studio_target"
| "error";
Expand Down
43 changes: 26 additions & 17 deletions apps/aevatar-console-web/src/pages/chat/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1174,7 +1174,7 @@ describe("ChatPage server-backed history", () => {
expect(screen.queryByRole("button", { name: "Retry" })).toBeNull();
});

it("keeps every chat action disabled until reconciliation observes a positive Conversation watermark", async () => {
it("keeps Team selection in the composer until reconciliation observes a positive Conversation watermark", async () => {
let resolveZeroDetail: (
detail: ReturnType<typeof conversationDetail>
) => void = () => undefined;
Expand All @@ -1195,11 +1195,13 @@ describe("ChatPage server-backed history", () => {
...serverConversation,
id: "server-confirm",
messageCount: 1,
title: "Confirmation plan",
title: "Weekly report Team selection",
};
const teamSelectionPrompt =
"请再确认具体使用哪个 Team(当前有两个 active Team)。\n确认后我会立即创建周报 workflow。";
const projectedMessages = [
{
content: "Please confirm this plan.",
content: teamSelectionPrompt,
id: "turn-confirm:assistant",
role: "assistant" as const,
status: "complete" as const,
Expand Down Expand Up @@ -1235,7 +1237,7 @@ describe("ChatPage server-backed history", () => {
chatContextFrame("server-confirm", "turn-confirm", "scope-a", 7),
{
runFinished: {
result: { output: "Please confirm this plan." },
result: { output: teamSelectionPrompt },
},
},
])
Expand All @@ -1250,40 +1252,47 @@ describe("ChatPage server-backed history", () => {
renderWithQueryClient(<ChatPage />);
await sendPrompt("Draft a workflow plan");

const confirmButton = await screen.findByRole("button", {
name: "Confirm and create",
});
expect(
await screen.findByText(
"请再确认具体使用哪个 Team(当前有两个 active Team)。"
)
).toBeInTheDocument();
expect(
screen.getByText("确认后我会立即创建周报 workflow。")
).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Confirm and create" })
).toBeNull();
expect(screen.queryByText("Needs confirmation")).toBeNull();
await waitFor(() =>
expect(chatHistoryApi.loadConversation).toHaveBeenCalledTimes(1)
);
expect(confirmButton).toBeDisabled();
expect(screen.getByRole("textbox")).toBeDisabled();

act(() => resolveZeroDetail(conversationDetail(projectedMessages, 0)));
await waitFor(() =>
expect(chatHistoryApi.loadConversation).toHaveBeenCalledTimes(2)
);
expect(confirmButton).toBeDisabled();
expect(screen.getByRole("textbox")).toBeDisabled();

act(() =>
resolveRegressedDetail(conversationDetail(projectedMessages, 6))
);
await waitFor(() =>
expect(
screen.getByRole("button", { name: "Confirm and create" })
).toBeEnabled()
);
expect(screen.getByRole("textbox")).toBeEnabled();
await waitFor(() => expect(screen.getByRole("textbox")).toBeEnabled());
expect(screen.getByText("Ready for reply")).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Confirm and create" })
).toBeNull();
expect(screen.queryByText("Needs confirmation")).toBeNull();

fireEvent.click(screen.getByRole("button", { name: "Confirm and create" }));
await sendPrompt("使用 Team team-weekly-report");
await waitFor(() => expect(chatRequestBodies()).toHaveLength(2));
expect(chatRequestBodies()[1]).toMatchObject({
conversation: {
conversationId: "server-confirm",
minimumStateVersion: 6,
},
prompt: "Confirm. Please create it now.",
prompt: "使用 Team team-weekly-report",
});
});

Expand Down
83 changes: 6 additions & 77 deletions apps/aevatar-console-web/src/pages/chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
MessageOutlined,
PlusOutlined,
ReloadOutlined,
SendOutlined,
} from "@ant-design/icons";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
Expand Down Expand Up @@ -466,14 +465,10 @@ function formatStatusLabel(status: LocalChatStatus): string {
return t("pages.chat.index.status.draft", "Draft");
case "streaming":
return t("pages.chat.index.status.streaming", "Streaming");
case "needs_confirmation":
return t("pages.chat.index.status.needsConfirmation", "Needs confirmation");
case "creating":
return t("pages.chat.index.status.creating", "Creating");
case "completed_with_studio_target":
return t("pages.chat.index.status.studioReady", "Studio ready");
case "completed_text":
return t("pages.chat.index.status.completed", "Completed");
return t("pages.chat.index.status.completed", "Ready for reply");
case "error":
return t("pages.chat.index.status.error", "Error");
default:
Expand All @@ -483,13 +478,10 @@ function formatStatusLabel(status: LocalChatStatus): string {

function resolveStatusTone(
status: LocalChatStatus
): "default" | "processing" | "success" | "warning" | "error" {
): "default" | "processing" | "success" | "error" {
switch (status) {
case "streaming":
case "creating":
return "processing";
case "needs_confirmation":
return "warning";
case "completed_with_studio_target":
case "completed_text":
return "success";
Expand All @@ -500,21 +492,6 @@ function resolveStatusTone(
}
}

function shouldAskForConfirmation(content: string): boolean {
const normalized = content.toLowerCase();
if (!normalized.trim()) {
return false;
}

return (
normalized.includes("confirm") ||
normalized.includes("approval") ||
normalized.includes("approve") ||
normalized.includes("确认") ||
normalized.includes("同意")
);
}

function resolveStudioJump(target: ChatStudioTarget | undefined): StudioJump | null {
if (!target) {
return null;
Expand Down Expand Up @@ -698,9 +675,7 @@ const ChatPage: React.FC = () => {
const conversations = (conversationsQuery.data ?? []).filter(
(conversation) => !scopedDeletedConversationIds.has(conversation.id)
);
const isStreaming =
activeConversation?.status === "streaming" ||
activeConversation?.status === "creating";
const isStreaming = activeConversation?.status === "streaming";
const studioJump = resolveStudioJump(activeConversation?.target);
const visibleConversations = useMemo<ConversationListItem[]>(() => {
const activeConversationId = activeConversation?.conversationId;
Expand Down Expand Up @@ -1227,8 +1202,6 @@ const ChatPage: React.FC = () => {
conversation.title === t("pages.chat.index.newChat", "New chat")
? trimTitle(trimmedInput)
: conversation.title;
const nextStatus: LocalChatStatus =
conversation.status === "needs_confirmation" ? "creating" : "streaming";
const createCommandId = conversation.conversationId
? undefined
: !conversation.createRequestPrompt ||
Expand All @@ -1243,7 +1216,7 @@ const ChatPage: React.FC = () => {
: trimmedInput,
latestTurnId: undefined,
messages: [...conversation.messages, userMessage, assistantMessage],
status: nextStatus,
status: "streaming",
title,
};
const rawFrames: unknown[] = [];
Expand Down Expand Up @@ -1451,14 +1424,11 @@ const ChatPage: React.FC = () => {
: "complete";
const finalTarget = artifacts.target || streamingConversation.target;
const finalUsage = artifacts.usage || streamingConversation.usage;
const finalContent = accumulator.finalOutput || accumulator.assistantText;
const finalStatus: LocalChatStatus = accumulator.errorText
? "error"
: resolveStudioJump(finalTarget)
? "completed_with_studio_target"
: shouldAskForConfirmation(finalContent)
? "needs_confirmation"
: "completed_text";
: "completed_text";
const finalConversation: ConversationState = {
...streamingConversation,
messages: streamingConversation.messages.map((message) =>
Expand Down Expand Up @@ -1664,17 +1634,6 @@ const ChatPage: React.FC = () => {
void runChat(conversation, prompt);
}, [activeConversation, prompt, runChat]);

const handleConfirmCreate = useCallback(() => {
if (!activeConversation) {
return;
}

void runChat(
activeConversation,
t("pages.chat.index.confirmPrompt", "Confirm. Please create it now.")
);
}, [activeConversation, runChat]);

const handleStop = useCallback(() => {
abortControllerRef.current?.abort();
}, []);
Expand Down Expand Up @@ -1871,8 +1830,7 @@ const ChatPage: React.FC = () => {
"pages.chat.index.historySavePendingShort",
"Saving history"
)
: conversation.liveStatus === "streaming" ||
conversation.liveStatus === "creating"
: conversation.liveStatus === "streaming"
? formatStatusLabel(conversation.liveStatus)
: formatTurnCount(conversation.messageCount)}
</span>
Expand Down Expand Up @@ -2214,35 +2172,6 @@ const ChatPage: React.FC = () => {
{activeConversation?.messages.map((message) => (
<ChatMessageEntry key={message.id} message={message} />
))}
{activeConversation?.status === "needs_confirmation" ? (
<div
style={{
background: token.colorWarningBg,
border: `1px solid ${token.colorWarningBorder}`,
borderRadius: token.borderRadius,
marginLeft: 34,
maxWidth: 760,
padding: 12,
}}
>
<Space direction="vertical" size={10}>
<Typography.Text strong>
{t(
"pages.chat.index.reviewPlan",
"Review the plan before creating resources."
)}
</Typography.Text>
<Button
disabled={isConversationActionDisabled}
icon={<SendOutlined />}
onClick={handleConfirmCreate}
type="primary"
>
{t("pages.chat.index.confirmAndCreate", "Confirm and create")}
</Button>
</Space>
</div>
) : null}
</Space>
)}
<div ref={scrollAnchorRef} />
Expand Down
Loading