fix title display mistake

This commit is contained in:
2026-07-10 17:15:53 +08:00
parent 79b21bc073
commit 5444b5cb39
11 changed files with 106 additions and 34 deletions
@@ -58,6 +58,7 @@ export interface EditorAgentMessage {
export interface EditorAgentConversationSummary {
conversationId: string;
projectId: string;
title: string;
updatedAt: string;
}
@@ -94,6 +95,7 @@ export interface EditorAgentMessageRequest {
}
export interface EditorAgentMessageResponse {
conversation: EditorAgentConversationSummary;
deltaMessages: EditorAgentMessage[];
errorMessage: string | null;
}
@@ -18,8 +18,8 @@ use shared_contracts::editor_agent::{
CreateEditorAgentConversationRequest, EditorAgentConversationListResponse,
EditorAgentConversationMessagesDocument, EditorAgentConversationResponse,
EditorAgentConversationSummary, EditorAgentGeneratedImage, EditorAgentMessage,
EditorAgentMessageRequest, EditorAgentMessageRole, EditorAgentToolCall,
EditorAgentToolCallStatus,
EditorAgentMessageRequest, EditorAgentMessageResponse, EditorAgentMessageRole,
EditorAgentToolCall, EditorAgentToolCallStatus,
};
use spacetime_client::{
EditorAgentConversationCreateRecordInput, EditorAgentConversationDeleteRecordInput,
@@ -34,11 +34,11 @@ use crate::editor_agent::editor_tools::common::EditorToolContext;
use crate::editor_agent::editor_tools::edit_image::{EditImageTool, EditImageToolArgs};
use crate::editor_agent::editor_tools::generate_image::{GenerateImageTool, GenerateImageToolArgs};
use crate::editor_agent::utils::{
EditorAgentMessageResponse, ImageId, ImageMetadata, IntoImageId,
build_editor_agent_canvas_completion, conversation_detail_from_record,
conversation_summary_from_record, editor_agent_bad_request, empty_messages_document,
ensure_editor_project_access, normalize_editor_agent_attachments, now_rfc3339,
read_messages_document, require_editor_agent_sidebar_enabled, write_messages_document,
ImageId, ImageMetadata, IntoImageId, build_editor_agent_canvas_completion,
conversation_detail_from_record, conversation_summary_from_record, editor_agent_bad_request,
empty_messages_document, ensure_editor_project_access, normalize_editor_agent_attachments,
now_rfc3339, read_messages_document, require_editor_agent_sidebar_enabled,
write_messages_document,
};
use crate::editor_project::{EditorGenerationCaller, current_utc_micros, map_editor_project_error};
use crate::http_error::AppError;
@@ -77,6 +77,8 @@ pub async fn editor_agent_message(
let mut document: EditorAgentConversationMessagesDocument =
read_messages_document(&state, &conversation).await?;
// Determine initialization before attachment bookkeeping adds a system message.
let was_empty = document.messages.is_empty();
let now = now_rfc3339();
if !attachments.is_empty() {
let mut attachment_info = String::new();
@@ -105,9 +107,7 @@ pub async fn editor_agent_message(
})
.collect();
// Save user message to document
let was_empty = document.messages.is_empty();
// TODO tell agent info about attachments
// Save user message to document.
let user_message = EditorAgentMessage {
id: document.messages.len(),
role: EditorAgentMessageRole::User,
@@ -119,19 +119,19 @@ pub async fn editor_agent_message(
document.messages.push(user_message.clone());
write_messages_document(&state, &conversation, &document).await?;
// Touch conversation (set title if first message)
if was_empty {
let title = derive_conversation_title(user_message.text.as_str());
let _ = state
// Persist and return the authoritative summary for every turn. Initialization sets the
// title from the first user prompt; a metadata write failure must fail the request.
let updated_conversation = state
.spacetime_client()
.touch_editor_agent_conversation(EditorAgentConversationTouchRecordInput {
conversation_id: conversation.conversation_id.clone(),
owner_user_id: conversation.owner_user_id.clone(),
title: Some(title),
title: was_empty.then(|| derive_conversation_title(user_message.text.as_str())),
updated_at_micros: current_utc_micros(),
})
.await;
}
.await
.map_err(map_editor_project_error)?;
let conversation_summary = conversation_summary_from_record(updated_conversation);
// Build tool context from document
let tool_context = build_tool_context(&document);
@@ -150,7 +150,9 @@ pub async fn editor_agent_message(
.tool(EditImageTool {
context: tool_context.clone(),
})
.tool(GenerateImageTool { context: tool_context })
.tool(GenerateImageTool {
context: tool_context,
})
.max_turns(3)
.memory(memory)
.build();
@@ -161,6 +163,7 @@ pub async fn editor_agent_message(
match build_delta_messages(agent_result, &assistant_now, document.messages.len()) {
Err(err) => Ok(Json(EditorAgentMessageResponse {
conversation: conversation_summary,
delta_messages: vec![],
error_message: Some(err.to_string()),
})),
@@ -171,6 +174,7 @@ pub async fn editor_agent_message(
write_messages_document(&state, &conversation, &document).await?;
Ok(Json(EditorAgentMessageResponse {
conversation: conversation_summary,
delta_messages,
error_message: None,
}))
@@ -529,7 +533,9 @@ pub async fn confirm_editor_agent_tool_call(
let canvas_completion =
build_editor_agent_canvas_completion(&project, GenerateImageTool::NAME, &title);
let tool_context = build_tool_context(&document);
let generate_tool = GenerateImageTool { context: tool_context };
let generate_tool = GenerateImageTool {
context: tool_context,
};
let result = generate_tool
.execute(
&state,
@@ -7,10 +7,10 @@ pub struct EditorToolContext {
pub images: HashMap<ImageId, ImageMetadata>,
}
impl EditorToolContext {
/// Check if an image with the given ID exists in the context.
pub fn contains_image(&self, image_id: &ImageId) -> bool {
self.images.contains_key(image_id)
}
}
@@ -128,13 +128,6 @@ impl Display for ImageId {
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentMessageResponse {
pub delta_messages: Vec<EditorAgentMessage>,
pub error_message: Option<String>,
}
type EditorAgentConversationLockMap = Mutex<BTreeMap<String, Arc<tokio::sync::Mutex<()>>>>;
static EDITOR_AGENT_CONVERSATION_LOCKS: OnceLock<EditorAgentConversationLockMap> = OnceLock::new();
@@ -536,6 +529,7 @@ pub fn conversation_summary_from_record(
EditorAgentConversationSummary {
conversation_id: conversation.conversation_id,
project_id: conversation.project_id,
title: conversation.title,
updated_at: conversation.updated_at,
}
}
@@ -102,6 +102,7 @@ pub struct EditorAgentMessage {
pub struct EditorAgentConversationSummary {
pub conversation_id: String,
pub project_id: String,
pub title: String,
pub updated_at: String,
}
@@ -150,3 +151,11 @@ pub struct EditorAgentMessageRequest {
#[serde(default)]
pub attachments: Vec<EditorAgentAttachmentRef>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentMessageResponse {
pub conversation: EditorAgentConversationSummary,
pub delta_messages: Vec<EditorAgentMessage>,
pub error_message: Option<String>,
}
@@ -53,6 +53,7 @@ function createClient(): EditorAgentConversationClient {
{
conversationId: 'conversation-1',
projectId: 'project-1',
title: '角色参考',
updatedAt: '2026-07-03T00:00:00.000Z',
},
]),
@@ -90,6 +91,12 @@ function createClient(): EditorAgentConversationClient {
updatedAt: '2026-07-03T00:00:00.000Z',
}),
sendMessage: vi.fn().mockResolvedValue({
conversation: {
conversationId: 'conversation-1',
projectId: 'project-1',
title: '角色参考',
updatedAt: '2026-07-03T00:00:10.000Z',
},
deltaMessages: [
{
id: 2,
@@ -331,6 +338,10 @@ describe('EditorAgentConversationPanelView', () => {
expect.any(Object),
);
});
expect(screen.getByRole('option', { name: '角色参考' })).toBeTruthy();
expect(
screen.queryByRole('option', { name: 'conversation-1' }),
).toBeNull();
});
it('sends selected attachments even when the text input is empty', async () => {
@@ -398,6 +409,12 @@ describe('EditorAgentConversationPanelView', () => {
it('preserves messages when the panel is collapsed and reopened', async () => {
const client = createClient();
vi.mocked(client.sendMessage).mockResolvedValue({
conversation: {
conversationId: 'conversation-1',
projectId: 'project-1',
title: '角色参考',
updatedAt: '2026-07-03T00:00:10.000Z',
},
deltaMessages: [
{
id: 3,
@@ -525,7 +525,7 @@ export function EditorAgentConversationPanelView({
key={conversation.conversationId}
value={conversation.conversationId}
>
{conversation.conversationId}
{conversation.title}
</option>
))
) : (
@@ -18,6 +18,7 @@ function createClient(): EditorAgentConversationClient {
{
conversationId: 'conversation-1',
projectId: 'project-1',
title: '角色参考',
updatedAt: '2026-07-03T00:00:00.000Z',
},
]),
@@ -46,6 +47,12 @@ function createClient(): EditorAgentConversationClient {
updatedAt: '2026-07-03T00:00:00.000Z',
}),
sendMessage: vi.fn().mockResolvedValue({
conversation: {
conversationId: 'conversation-1',
projectId: 'project-1',
title: '把这个角色改成像素风',
updatedAt: '2026-07-03T00:00:01.000Z',
},
deltaMessages: [
{
id: 1,
@@ -115,6 +122,9 @@ describe('useEditorAgentConversation', () => {
}),
);
expect(result.current.isWaiting).toBe(false);
expect(result.current.activeConversation?.title).toBe(
'把这个角色改成像素风',
);
expect(onCanvasRefreshRequested).toHaveBeenCalledTimes(1);
expect(result.current.messages.map((message) => message.text)).toEqual([
'把这个角色改成像素风',
@@ -199,6 +209,12 @@ describe('useEditorAgentConversation', () => {
it('handles backend error responses', async () => {
const client = createClient();
vi.mocked(client.sendMessage).mockResolvedValue({
conversation: {
conversationId: 'conversation-1',
projectId: 'project-1',
title: '这是美术素材',
updatedAt: '2026-07-03T00:00:01.000Z',
},
deltaMessages: [],
errorMessage: 'LLM 未配置,无法处理这句话。',
} as EditorAgentMessageResponse);
@@ -222,6 +238,12 @@ describe('useEditorAgentConversation', () => {
it('confirms a pending tool call, replaces its message and requests a canvas refresh', async () => {
const client = createClient();
vi.mocked(client.sendMessage).mockResolvedValue({
conversation: {
conversationId: 'conversation-1',
projectId: 'project-1',
title: '角色参考',
updatedAt: '2026-07-03T00:00:01.000Z',
},
deltaMessages: [
{
id: 1,
@@ -113,6 +113,7 @@ function summaryFromDetail(
return {
conversationId: detail.conversationId,
projectId: detail.projectId,
title: detail.title,
updatedAt: detail.updatedAt,
};
}
@@ -362,6 +363,12 @@ export function useEditorAgentConversation({
},
);
setConversations((currentConversations) =>
upsertConversationSummary(
currentConversations,
response.conversation,
),
);
if (response.errorMessage) {
setErrorMessage(response.errorMessage);
} else {
@@ -68,6 +68,7 @@ const getEditorAgentConversationMock = vi.hoisted(() =>
>(async () => ({
conversationId: 'editor-agent-conv-test',
projectId: 'editor-project-default',
title: '画布 Agent',
updatedAt: '2026-07-03T00:00:00.000Z',
messages: [],
})),
@@ -89,6 +90,12 @@ const sendEditorAgentMessageMock = vi.hoisted(() =>
Parameters<EditorAgentStreamMessage>,
ReturnType<EditorAgentStreamMessage>
>(async () => ({
conversation: {
conversationId: 'editor-agent-conv-test',
projectId: 'editor-project-default',
title: '画布 Agent',
updatedAt: '2026-07-03T00:00:00.000Z',
},
deltaMessages: [],
errorMessage: null,
})),
@@ -163,6 +170,7 @@ function createEditorAgentConversationSummary(
return {
conversationId: 'editor-agent-conv-test',
projectId: 'editor-project-default',
title: '画布 Agent',
updatedAt: '2026-07-03T00:00:00.000Z',
...overrides,
};
@@ -274,6 +282,7 @@ describe('ImageCanvasEditorView', () => {
createEditorAgentConversationSummary(),
);
sendEditorAgentMessageMock.mockResolvedValue({
conversation: createEditorAgentConversationSummary(),
deltaMessages: [],
errorMessage: null,
});
@@ -102,6 +102,12 @@ describe('editorAgentClient', () => {
it('sends an editor agent message and returns delta messages', async () => {
const responseBody = {
conversation: {
conversationId: 'conversation-1',
projectId: 'project-1',
title: '帮我把角色改成像素风',
updatedAt: '2026-07-03T00:00:01.000Z',
},
deltaMessages: [
{
id: 1,