Make new chat creation lazy

This commit is contained in:
Xisheng-Zhao
2026-06-08 17:19:07 +08:00
parent af7e92b934
commit 652c4826b1
2 changed files with 81 additions and 31 deletions
@@ -21,6 +21,14 @@ final class ChatExperiencePolicy {
return ""; return "";
} }
static String firstChatTitle(String content) {
String clean = content == null ? "" : content.trim();
if (clean.isEmpty()) return "";
StringBuilder title = new StringBuilder();
clean.codePoints().limit(15).forEach(title::appendCodePoint);
return title.toString();
}
static boolean requiresTitleBeforeEnteringChat() { static boolean requiresTitleBeforeEnteringChat() {
return false; return false;
} }
@@ -399,6 +399,16 @@ private data class PilotChatSessionsData(
val agents: List<Models.Agent>, val agents: List<Models.Agent>,
) )
private fun localDraftChatSession(agent: Models.Agent): Models.ChatSession =
Models.ChatSession(
JSONObject()
.put("id", "")
.put("title", "")
.put("agent_id", agent.id)
.put("status", "draft")
.put("updated_at", ""),
)
private data class PilotProjectDetailData( private data class PilotProjectDetailData(
val project: Models.Project, val project: Models.Project,
val issues: List<Models.Issue>, val issues: List<Models.Issue>,
@@ -3007,7 +3017,6 @@ private fun PilotChatSessions(
var state by remember(workspaceId) { mutableStateOf<Result<PilotChatSessionsData>?>(null) } var state by remember(workspaceId) { mutableStateOf<Result<PilotChatSessionsData>?>(null) }
var refresh by remember(workspaceId) { mutableIntStateOf(0) } var refresh by remember(workspaceId) { mutableIntStateOf(0) }
var showAllSessions by remember(workspaceId) { mutableStateOf(false) } var showAllSessions by remember(workspaceId) { mutableStateOf(false) }
var creating by remember(workspaceId) { mutableStateOf(false) }
var deletingSessionId by remember(workspaceId) { mutableStateOf<String?>(null) } var deletingSessionId by remember(workspaceId) { mutableStateOf<String?>(null) }
var actionText by remember(workspaceId) { mutableStateOf<String?>(null) } var actionText by remember(workspaceId) { mutableStateOf<String?>(null) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -3028,29 +3037,14 @@ private fun PilotChatSessions(
state = loaded state = loaded
} }
fun createSession(data: PilotChatSessionsData) { fun openDraftSession(data: PilotChatSessionsData) {
if (creating) return
val agent = data.agents.firstOrNull() val agent = data.agents.firstOrNull()
if (agent == null) { if (agent == null) {
actionText = if (zh) "请先创建 Agent" else "Create an agent first" actionText = if (zh) "请先创建 Agent" else "Create an agent first"
return return
} }
val sessionTitle = ChatExperiencePolicy.newChatInitialTitle()
creating = true
actionText = null actionText = null
scope.launch { onSessionClick(localDraftChatSession(agent))
val result = withContext(Dispatchers.IO) {
runCatching { api.createChatSession(workspaceId, agent.id, sessionTitle) }
}
creating = false
result.onSuccess { session ->
actionText = if (zh) "聊天会话已创建" else "Chat session created"
refresh++
onSessionClick(session)
}.onFailure {
actionText = "${if (zh) "创建聊天失败" else "Create chat failed"}: ${it.message ?: it.toString()}"
}
}
} }
fun deleteSession(session: Models.ChatSession) { fun deleteSession(session: Models.ChatSession) {
@@ -3096,13 +3090,12 @@ private fun PilotChatSessions(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
MulticaPillButton( MulticaPillButton(
text = if (creating) "..." else if (zh) "新聊天" else "New Chat", text = if (zh) "新聊天" else "New Chat",
onClick = { onClick = {
createSession(data) openDraftSession(data)
actionText = null actionText = null
}, },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
enabled = !creating,
tone = MulticaButtonTone.Primary, tone = MulticaButtonTone.Primary,
contentDescription = "Chat New Session Toggle", contentDescription = "Chat New Session Toggle",
) )
@@ -3419,6 +3412,7 @@ private fun PilotChatMessages(
onSessionChanged: (Models.ChatSession) -> Unit, onSessionChanged: (Models.ChatSession) -> Unit,
onArchived: () -> Unit, onArchived: () -> Unit,
) { ) {
val isDraftSession = session.id.isBlank()
var state by remember(session.id) { mutableStateOf<Result<PilotChatMessagesData>?>(null) } var state by remember(session.id) { mutableStateOf<Result<PilotChatMessagesData>?>(null) }
var messagesRefresh by remember(session.id) { mutableIntStateOf(0) } var messagesRefresh by remember(session.id) { mutableIntStateOf(0) }
var pendingRefresh by remember(session.id) { mutableIntStateOf(0) } var pendingRefresh by remember(session.id) { mutableIntStateOf(0) }
@@ -3432,6 +3426,7 @@ private fun PilotChatMessages(
var agentPickerOpen by remember(session.id) { mutableStateOf(false) } var agentPickerOpen by remember(session.id) { mutableStateOf(false) }
var agentQuery by remember(session.id) { mutableStateOf("") } var agentQuery by remember(session.id) { mutableStateOf("") }
var switchingAgentId by remember(session.id) { mutableStateOf<String?>(null) } var switchingAgentId by remember(session.id) { mutableStateOf<String?>(null) }
var draftAgentId by remember(session.id, session.agentId) { mutableStateOf(session.agentId) }
var composerFocused by remember(session.id) { mutableStateOf(false) } var composerFocused by remember(session.id) { mutableStateOf(false) }
var actionError by remember(session.id) { mutableStateOf<String?>(null) } var actionError by remember(session.id) { mutableStateOf<String?>(null) }
val configuration = LocalConfiguration.current val configuration = LocalConfiguration.current
@@ -3448,12 +3443,19 @@ private fun PilotChatMessages(
val previous = state val previous = state
val result = withContext(Dispatchers.IO) { val result = withContext(Dispatchers.IO) {
runCatching { runCatching {
val messages = api.chatMessages(workspaceId, session.id)
val pending = runCatching { api.pendingChatTask(workspaceId, session.id) }
.getOrElse { Models.ChatPendingTask(org.json.JSONObject()) }
val agents = api.agents(workspaceId) val agents = api.agents(workspaceId)
.filter { it.archivedAt.isBlank() } .filter { it.archivedAt.isBlank() }
.sortedBy { it.name.lowercase() } .sortedBy { it.name.lowercase() }
if (isDraftSession) {
return@runCatching PilotChatMessagesData(
emptyList(),
Models.ChatPendingTask(org.json.JSONObject()),
agents,
)
}
val messages = api.chatMessages(workspaceId, session.id)
val pending = runCatching { api.pendingChatTask(workspaceId, session.id) }
.getOrElse { Models.ChatPendingTask(org.json.JSONObject()) }
runCatching { api.markChatSessionRead(workspaceId, session.id) } runCatching { api.markChatSessionRead(workspaceId, session.id) }
PilotChatMessagesData(messages, pending, agents) PilotChatMessagesData(messages, pending, agents)
} }
@@ -3466,6 +3468,7 @@ private fun PilotChatMessages(
} }
LaunchedEffect(session.id, pendingRefresh) { LaunchedEffect(session.id, pendingRefresh) {
if (isDraftSession) return@LaunchedEffect
val current = state?.getOrNull() ?: return@LaunchedEffect val current = state?.getOrNull() ?: return@LaunchedEffect
val previousPendingTaskId = current.pendingTask.taskId val previousPendingTaskId = current.pendingTask.taskId
val result = withContext(Dispatchers.IO) { val result = withContext(Dispatchers.IO) {
@@ -3523,6 +3526,10 @@ private fun PilotChatMessages(
} }
fun archiveSession() { fun archiveSession() {
if (isDraftSession) {
onArchived()
return
}
if (archiving) return if (archiving) return
archiving = true archiving = true
headerMenuOpen = false headerMenuOpen = false
@@ -3558,6 +3565,13 @@ private fun PilotChatMessages(
} }
fun switchAgent(agent: Models.Agent) { fun switchAgent(agent: Models.Agent) {
if (isDraftSession) {
draftAgentId = agent.id
agentPickerOpen = false
agentQuery = ""
actionError = null
return
}
if (agent.id == session.agentId || switchingAgentId != null) { if (agent.id == session.agentId || switchingAgentId != null) {
agentPickerOpen = false agentPickerOpen = false
return return
@@ -3601,7 +3615,11 @@ private fun PilotChatMessages(
.semantics { contentDescription = "Chat Detail Header Safe Horizontal Insets" }, .semantics { contentDescription = "Chat Detail Header Safe Horizontal Insets" },
) { ) {
PilotPageHeader( PilotPageHeader(
title = session.title, title = if (isDraftSession) {
if (zh) "新聊天" else "New Chat"
} else {
session.title
},
leading = { PilotSecondaryBackButton(zh = zh, onBack = onBack) }, leading = { PilotSecondaryBackButton(zh = zh, onBack = onBack) },
trailing = { trailing = {
MulticaIconPillButton( MulticaIconPillButton(
@@ -3618,7 +3636,7 @@ private fun PilotChatMessages(
MulticaCupertinoActionSheetItem( MulticaCupertinoActionSheetItem(
title = if (archiving) "..." else if (zh) "归档会话" else "Archive Session", title = if (archiving) "..." else if (zh) "归档会话" else "Archive Session",
icon = Icons.Outlined.Archive, icon = Icons.Outlined.Archive,
enabled = !archiving, enabled = !isDraftSession && !archiving,
contentDescription = "Chat Header Cupertino Action Sheet Archive Session", contentDescription = "Chat Header Cupertino Action Sheet Archive Session",
onClick = { archiveSession() }, onClick = { archiveSession() },
), ),
@@ -3652,7 +3670,8 @@ private fun PilotChatMessages(
) )
} }
loaded?.getOrNull()?.let { chatData -> loaded?.getOrNull()?.let { chatData ->
val activeAgent = chatData.agents.firstOrNull { it.id == session.agentId } ?: chatData.agents.firstOrNull() val selectedAgentId = if (isDraftSession) draftAgentId else session.agentId
val activeAgent = chatData.agents.firstOrNull { it.id == selectedAgentId } ?: chatData.agents.firstOrNull()
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -3832,18 +3851,41 @@ private fun PilotChatMessages(
onClick = { onClick = {
val content = draft.trim() val content = draft.trim()
if (content.isEmpty() || sending) return@MulticaIconPillButton if (content.isEmpty() || sending) return@MulticaIconPillButton
val targetAgentId = if (isDraftSession) {
draftAgentId.ifBlank { state?.getOrNull()?.agents?.firstOrNull()?.id.orEmpty() }
} else {
session.agentId
}
if (isDraftSession && targetAgentId.isBlank()) {
sendError = if (zh) "请先创建 Agent" else "Create an agent first"
return@MulticaIconPillButton
}
sending = true sending = true
locallyPending = true locallyPending = true
sendError = null sendError = null
scope.launch { scope.launch {
val result = withContext(Dispatchers.IO) { val result = withContext(Dispatchers.IO) {
runCatching { api.sendChatMessage(workspaceId, session.id, content) } runCatching {
if (isDraftSession) {
val title = ChatExperiencePolicy.firstChatTitle(content)
val created = api.createChatSession(workspaceId, targetAgentId, title)
api.sendChatMessage(workspaceId, created.id, content)
created
} else {
api.sendChatMessage(workspaceId, session.id, content)
session
}
}
} }
sending = false sending = false
result.onSuccess { result.onSuccess { created ->
draft = "" draft = ""
pendingRefresh++ if (isDraftSession) {
messagesRefresh++ onSessionChanged(created)
} else {
pendingRefresh++
messagesRefresh++
}
}.onFailure { }.onFailure {
locallyPending = false locallyPending = false
sendError = "${if (zh) "发送失败" else "Send failed"}: ${it.message ?: it.toString()}" sendError = "${if (zh) "发送失败" else "Send failed"}: ${it.message ?: it.toString()}"