Add squad mentions and assignee support
This commit is contained in:
@@ -748,6 +748,18 @@ final class ApiClient {
|
||||
return parseArray(extractArray(json, "agents"), Models.Agent::new);
|
||||
}
|
||||
|
||||
|
||||
List<Models.Squad> squads(String workspaceId) throws Exception {
|
||||
return squads(workspaceId, false);
|
||||
}
|
||||
|
||||
List<Models.Squad> squads(String workspaceId, boolean includeArchived) throws Exception {
|
||||
JSONObject query = query(null, "workspace_id", workspaceId);
|
||||
if (includeArchived) query.put("include_archived", "true");
|
||||
JSONObject json = requestObject("GET", "/api/squads", query, null);
|
||||
return parseArray(extractArray(json, "squads"), Models.Squad::new);
|
||||
}
|
||||
|
||||
List<Models.Runtime> runtimes(String workspaceId) throws Exception {
|
||||
JSONObject json = requestObject("GET", "/api/runtimes", query(null, "workspace_id", workspaceId), null);
|
||||
return parseArray(extractArray(json, "runtimes"), Models.Runtime::new);
|
||||
|
||||
@@ -33,6 +33,8 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -367,6 +369,7 @@ private data class PilotIssueDetailData(
|
||||
val projects: List<Models.Project>,
|
||||
val members: List<Models.Member>,
|
||||
val agents: List<Models.Agent>,
|
||||
val squads: List<Models.Squad>,
|
||||
val issueLabels: List<Models.IssueLabel>,
|
||||
val allLabels: List<Models.IssueLabel>,
|
||||
val subscribers: List<Models.IssueSubscriber>,
|
||||
@@ -381,6 +384,7 @@ private data class PilotIssueFormOptions(
|
||||
val projects: List<Models.Project>,
|
||||
val members: List<Models.Member>,
|
||||
val agents: List<Models.Agent>,
|
||||
val squads: List<Models.Squad>,
|
||||
)
|
||||
|
||||
private data class PilotChatMessagesData(
|
||||
@@ -5622,6 +5626,7 @@ private fun PilotIssues(
|
||||
var movingBoardIssueId by remember(personal, workspaceId) { mutableStateOf<String?>(null) }
|
||||
var assigneeMembers by remember(workspaceId) { mutableStateOf<List<Models.Member>>(emptyList()) }
|
||||
var assigneeAgents by remember(workspaceId) { mutableStateOf<List<Models.Agent>>(emptyList()) }
|
||||
var assigneeSquads by remember(workspaceId) { mutableStateOf<List<Models.Squad>>(emptyList()) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(personal, myIssueScope, issues) {
|
||||
@@ -5733,6 +5738,7 @@ private fun PilotIssues(
|
||||
}.onSuccess { (members, agents) ->
|
||||
assigneeMembers = members
|
||||
assigneeAgents = agents
|
||||
assigneeSquads = runCatching { api.squads(workspaceId) }.getOrElse { emptyList() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6070,6 +6076,7 @@ private fun PilotIssues(
|
||||
issue,
|
||||
assigneeMembers,
|
||||
assigneeAgents,
|
||||
assigneeSquads,
|
||||
currentUser,
|
||||
)
|
||||
if (isNotBlank()) append(" · ")
|
||||
@@ -6772,6 +6779,7 @@ private fun PilotIssueForm(
|
||||
projects = api.projects(session.workspace.id, 500, 0).items,
|
||||
members = api.members(session.workspace.id),
|
||||
agents = api.agents(session.workspace.id),
|
||||
squads = runCatching { api.squads(session.workspace.id) }.getOrElse { emptyList() },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -6792,6 +6800,7 @@ private fun PilotIssueForm(
|
||||
session.user,
|
||||
options.members,
|
||||
options.agents,
|
||||
options.squads,
|
||||
).map { PilotAssigneeOption(it.id, it.type, it.name) }
|
||||
var title by remember(issue?.id) { mutableStateOf(issue?.title.orEmpty()) }
|
||||
var description by remember(issue?.id) { mutableStateOf(issue?.description.orEmpty()) }
|
||||
@@ -8868,6 +8877,7 @@ private fun IssueCommentCard(
|
||||
authorAgentStatus: String? = null,
|
||||
members: List<Models.Member> = emptyList(),
|
||||
agents: List<Models.Agent> = emptyList(),
|
||||
squads: List<Models.Squad> = emptyList(),
|
||||
currentUser: Models.User? = null,
|
||||
onIssueIdClick: ((String) -> Unit)? = null,
|
||||
onIssueReferenceClick: ((String) -> Unit)? = null,
|
||||
@@ -8962,6 +8972,7 @@ private fun IssueCommentCard(
|
||||
renderedContent,
|
||||
members = members,
|
||||
agents = agents,
|
||||
squads = squads,
|
||||
currentUser = currentUser,
|
||||
attachments = comment.attachments,
|
||||
onIssueIdClick = onIssueIdClick,
|
||||
@@ -9387,6 +9398,7 @@ private fun PilotIssueDetail(
|
||||
projects = core.projects,
|
||||
members = core.members,
|
||||
agents = core.agents,
|
||||
squads = emptyList(),
|
||||
issueLabels = emptyList(),
|
||||
allLabels = emptyList(),
|
||||
subscribers = emptyList(),
|
||||
@@ -9417,9 +9429,11 @@ private fun PilotIssueDetail(
|
||||
val timelineAnchor = highlightCommentId ?: core.comments.lastOrNull()?.id
|
||||
val timeline = runCatching { api.issueTimeline(workspaceId, issueId, 10, null, null, timelineAnchor) }.getOrNull()
|
||||
val activeTasks = runCatching { api.activeIssueTasks(workspaceId, issueId) }.getOrElse { emptyList() }
|
||||
val squads = runCatching { api.squads(workspaceId) }.getOrElse { emptyList() }
|
||||
initialData.copy(
|
||||
parentIssue = parentIssue,
|
||||
projects = projects,
|
||||
squads = squads,
|
||||
issueLabels = issueLabels,
|
||||
allLabels = allLabels,
|
||||
subscribers = subscribers,
|
||||
@@ -9651,7 +9665,7 @@ private fun PilotIssueDetail(
|
||||
Models.Assignee(
|
||||
assigneeId,
|
||||
data.issue.assigneeType ?: "member",
|
||||
Models.issueAssigneeDisplayName(data.issue, data.members, data.agents, currentUser),
|
||||
Models.issueAssigneeDisplayName(data.issue, data.members, data.agents, data.squads, currentUser),
|
||||
)
|
||||
}
|
||||
scope.launch {
|
||||
@@ -9803,7 +9817,7 @@ private fun PilotIssueDetail(
|
||||
}
|
||||
|
||||
fun sendNewComment() {
|
||||
val content = AgentMentionMarkdown.markdownFromDraft(newCommentDraft.trim(), data.agents)
|
||||
val content = AgentMentionMarkdown.markdownFromDraft(newCommentDraft.trim(), data.agents, data.members, data.squads)
|
||||
if (sendingNewComment || content.isEmpty() && pendingCommentAttachments.isEmpty()) return
|
||||
sendingNewComment = true
|
||||
newCommentMessage = null
|
||||
@@ -9826,7 +9840,7 @@ private fun PilotIssueDetail(
|
||||
|
||||
fun sendReply(thread: IssueCommentThreads.Thread) {
|
||||
val parentId = thread.root.id
|
||||
val explicitContent = AgentMentionMarkdown.markdownFromDraft(replyDraft.trim(), data.agents)
|
||||
val explicitContent = AgentMentionMarkdown.markdownFromDraft(replyDraft.trim(), data.agents, data.members, data.squads)
|
||||
val content = IssueCommentThreads.replyContentForThread(thread, explicitContent, data.agents)
|
||||
if (sendingReply || (content.isEmpty() && pendingReplyAttachments.isEmpty())) return
|
||||
sendingReply = true
|
||||
@@ -9931,6 +9945,8 @@ private fun PilotIssueDetail(
|
||||
draft = replyDraft,
|
||||
pendingAttachments = pendingReplyAttachments,
|
||||
agents = data.agents,
|
||||
members = data.members,
|
||||
squads = data.squads,
|
||||
uploadingAttachment = uploadingReplyAttachment,
|
||||
sending = sendingReply,
|
||||
zh = zh,
|
||||
@@ -9954,6 +9970,8 @@ private fun PilotIssueDetail(
|
||||
draft = newCommentDraft,
|
||||
pendingAttachments = pendingCommentAttachments,
|
||||
agents = data.agents,
|
||||
members = data.members,
|
||||
squads = data.squads,
|
||||
uploadingAttachment = uploadingCommentAttachment,
|
||||
sending = sendingNewComment,
|
||||
zh = zh,
|
||||
@@ -9988,7 +10006,7 @@ private fun PilotIssueDetail(
|
||||
IssueDetailCompactMetadata(
|
||||
issue = data.issue,
|
||||
issueLabels = data.issueLabels,
|
||||
assignee = Models.issueAssigneeDisplayName(data.issue, data.members, data.agents, currentUser),
|
||||
assignee = Models.issueAssigneeDisplayName(data.issue, data.members, data.agents, data.squads, currentUser),
|
||||
project = projectName,
|
||||
zh = zh,
|
||||
)
|
||||
@@ -10026,6 +10044,7 @@ private fun PilotIssueDetail(
|
||||
data.issue.description,
|
||||
members = data.members,
|
||||
agents = data.agents,
|
||||
squads = data.squads,
|
||||
currentUser = currentUser,
|
||||
attachments = data.attachments,
|
||||
onIssueIdClick = { onOpenIssue(it) },
|
||||
@@ -10185,6 +10204,7 @@ private fun PilotIssueDetail(
|
||||
authorAgentStatus = commentAuthorAgentStatus(comment, data.agents),
|
||||
members = data.members,
|
||||
agents = data.agents,
|
||||
squads = data.squads,
|
||||
currentUser = currentUser,
|
||||
onIssueIdClick = { onOpenIssue(it) },
|
||||
onIssueReferenceClick = { openIssueReference(it) },
|
||||
@@ -10741,6 +10761,8 @@ private fun IssueCommentReplyBox(
|
||||
draft: String,
|
||||
pendingAttachments: List<Models.Attachment>,
|
||||
agents: List<Models.Agent>,
|
||||
members: List<Models.Member>,
|
||||
squads: List<Models.Squad>,
|
||||
uploadingAttachment: Boolean,
|
||||
sending: Boolean,
|
||||
zh: Boolean,
|
||||
@@ -10758,9 +10780,9 @@ private fun IssueCommentReplyBox(
|
||||
val keyboard = LocalSoftwareKeyboardController.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val replyActionModifier = Modifier.size(42.dp)
|
||||
val mentionAgents = remember(agents, mentionQuery) {
|
||||
val mentionItems = remember(agents, members, squads, mentionQuery) {
|
||||
val query = mentionQuery.trim().lowercase()
|
||||
agents
|
||||
val agentItems = agents
|
||||
.filter { it.archivedAt.isBlank() }
|
||||
.filter { agent ->
|
||||
query.isBlank() ||
|
||||
@@ -10769,7 +10791,24 @@ private fun IssueCommentReplyBox(
|
||||
agent.id.lowercase().contains(query)
|
||||
}
|
||||
.sortedBy { it.name.lowercase() }
|
||||
.take(8)
|
||||
.map { Triple(it.id, it.name.ifBlank { Models.shortId(it.id) }, "agent") }
|
||||
val memberItems = members
|
||||
.filter { member ->
|
||||
query.isBlank() ||
|
||||
member.displayName.lowercase().contains(query) ||
|
||||
member.email.lowercase().contains(query)
|
||||
}
|
||||
.sortedBy { it.displayName.lowercase() }
|
||||
.map { Triple(it.id, it.displayName, "member") }
|
||||
val squadItems = squads
|
||||
.filter { squad ->
|
||||
query.isBlank() ||
|
||||
squad.name.lowercase().contains(query) ||
|
||||
squad.description.lowercase().contains(query)
|
||||
}
|
||||
.sortedBy { it.name.lowercase() }
|
||||
.map { Triple(it.id, it.name.ifBlank { Models.shortId(it.id) }, "squad") }
|
||||
agentItems + memberItems + squadItems
|
||||
}
|
||||
LaunchedEffect(parentId) {
|
||||
delay(180)
|
||||
@@ -10871,11 +10910,12 @@ private fun IssueCommentReplyBox(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.imePadding()
|
||||
.semantics { contentDescription = "Issue Comment Reply Mention Web Picker" },
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = if (zh) "选择 Agent" else "Mention agent",
|
||||
text = if (zh) "选择 @对象" else "Mention someone",
|
||||
modifier = Modifier.semantics { contentDescription = "Issue Comment Mention Web Header" },
|
||||
style = MaterialTheme.typography.labelMedium.copy(fontSize = 12.sp, lineHeight = 15.sp, fontWeight = FontWeight.SemiBold),
|
||||
color = MulticaColors.Text,
|
||||
@@ -10885,22 +10925,30 @@ private fun IssueCommentReplyBox(
|
||||
onValueChange = { mentionQuery = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentDescription = "Issue Comment Reply Mention Search",
|
||||
label = if (zh) "搜索 Agent" else "Search agents",
|
||||
label = if (zh) "搜索人、Agent、小队" else "Search people, agents, squads",
|
||||
maxLines = 1,
|
||||
)
|
||||
if (mentionAgents.isEmpty()) {
|
||||
if (mentionItems.isEmpty()) {
|
||||
Text(
|
||||
text = if (zh) "没有可提及的 Agent" else "No agents available",
|
||||
text = if (zh) "没有可提及的对象" else "No mentionable items",
|
||||
style = MaterialTheme.typography.bodySmall.copy(fontSize = 12.sp, lineHeight = 15.sp),
|
||||
color = MulticaColors.Muted,
|
||||
)
|
||||
} else {
|
||||
mentionAgents.forEach { agent ->
|
||||
IssueMentionAgentWebRow(
|
||||
agent = agent,
|
||||
contentDescription = "Issue Comment Reply Mention Agent Row ${agent.id}",
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 240.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
mentionItems.forEach { (id, name, type) ->
|
||||
IssueMentionUnifiedRow(
|
||||
name = name,
|
||||
mentionType = type,
|
||||
contentDescription = "Issue Comment Reply Mention $type Row $id",
|
||||
onClick = {
|
||||
val insertion = AgentMentionMarkdown.displayText(agent.name, agent.id)
|
||||
val insertion = AgentMentionMarkdown.displayText(name, id)
|
||||
val separator = if (draft.isBlank() || draft.endsWith(" ") || draft.endsWith("\n")) "" else " "
|
||||
onDraftChange(draft + separator + insertion + " ")
|
||||
mentionPickerOpen = false
|
||||
@@ -10914,6 +10962,7 @@ private fun IssueCommentReplyBox(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IssueMentionAgentWebRow(
|
||||
@@ -10985,11 +11034,110 @@ private fun IssueMentionAgentWebRow(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IssueMentionUnifiedRow(
|
||||
name: String,
|
||||
mentionType: String,
|
||||
contentDescription: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val iconLetter = when (mentionType) {
|
||||
"member" -> name.take(1).uppercase().ifBlank { "M" }
|
||||
"squad" -> name.take(1).uppercase().ifBlank { "S" }
|
||||
else -> name.take(1).uppercase().ifBlank { "A" }
|
||||
}
|
||||
val iconColor = when (mentionType) {
|
||||
"member" -> MulticaColors.Text
|
||||
"squad" -> MulticaColors.Accent
|
||||
else -> MulticaColors.Accent
|
||||
}
|
||||
val iconBg = when (mentionType) {
|
||||
"member" -> MulticaColors.AccentSoft.copy(alpha = 0.12f)
|
||||
"squad" -> MulticaColors.AccentSoft.copy(alpha = 0.36f)
|
||||
else -> MulticaColors.AccentSoft.copy(alpha = 0.24f)
|
||||
}
|
||||
val typeLabel = when (mentionType) {
|
||||
"member" -> "member"
|
||||
"squad" -> "squad"
|
||||
else -> "agent"
|
||||
}
|
||||
val mentionMarkdown = when (mentionType) {
|
||||
"member" -> AgentMentionMarkdown.memberMarkdown(name, "")
|
||||
"squad" -> AgentMentionMarkdown.squadMarkdown(name, "")
|
||||
else -> AgentMentionMarkdown.markdown(name, "")
|
||||
}
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.semantics(mergeDescendants = true) {
|
||||
this.contentDescription = contentDescription
|
||||
semanticsOnClick(label = name) {
|
||||
onClick()
|
||||
true
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = MulticaColors.SurfaceElevated.copy(alpha = 0.78f),
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.border(1.dp, MulticaColors.Border.copy(alpha = 0.58f), RoundedCornerShape(8.dp))
|
||||
.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(9.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(27.dp)
|
||||
.clip(RoundedCornerShape(999.dp))
|
||||
.background(iconBg),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = iconLetter,
|
||||
style = MaterialTheme.typography.labelMedium.copy(fontSize = 11.sp, lineHeight = 13.sp, fontWeight = FontWeight.SemiBold),
|
||||
color = iconColor,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(
|
||||
text = name,
|
||||
style = MaterialTheme.typography.titleSmall.copy(fontSize = 13.sp, lineHeight = 16.sp, fontWeight = FontWeight.SemiBold),
|
||||
color = MulticaColors.Text,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
text = typeLabel,
|
||||
style = MaterialTheme.typography.labelSmall.copy(fontSize = 10.sp, lineHeight = 12.sp),
|
||||
color = MulticaColors.Muted,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.KeyboardArrowRight,
|
||||
contentDescription = null,
|
||||
tint = MulticaColors.TextTertiary,
|
||||
modifier = Modifier.size(17.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun IssueCommentInputBar(
|
||||
draft: String,
|
||||
pendingAttachments: List<Models.Attachment>,
|
||||
agents: List<Models.Agent>,
|
||||
members: List<Models.Member>,
|
||||
squads: List<Models.Squad>,
|
||||
uploadingAttachment: Boolean,
|
||||
sending: Boolean,
|
||||
zh: Boolean,
|
||||
@@ -11000,9 +11148,9 @@ private fun IssueCommentInputBar(
|
||||
) {
|
||||
var mentionPickerOpen by remember { mutableStateOf(false) }
|
||||
var mentionQuery by remember { mutableStateOf("") }
|
||||
val mentionAgents = remember(agents, mentionQuery) {
|
||||
val mentionItems = remember(agents, members, squads, mentionQuery) {
|
||||
val query = mentionQuery.trim().lowercase()
|
||||
agents
|
||||
val agentItems = agents
|
||||
.filter { it.archivedAt.isBlank() }
|
||||
.filter { agent ->
|
||||
query.isBlank() ||
|
||||
@@ -11011,7 +11159,24 @@ private fun IssueCommentInputBar(
|
||||
agent.id.lowercase().contains(query)
|
||||
}
|
||||
.sortedBy { it.name.lowercase() }
|
||||
.take(12)
|
||||
.map { Triple(it.id, it.name.ifBlank { Models.shortId(it.id) }, "agent") }
|
||||
val memberItems = members
|
||||
.filter { member ->
|
||||
query.isBlank() ||
|
||||
member.displayName.lowercase().contains(query) ||
|
||||
member.email.lowercase().contains(query)
|
||||
}
|
||||
.sortedBy { it.displayName.lowercase() }
|
||||
.map { Triple(it.id, it.displayName, "member") }
|
||||
val squadItems = squads
|
||||
.filter { squad ->
|
||||
query.isBlank() ||
|
||||
squad.name.lowercase().contains(query) ||
|
||||
squad.description.lowercase().contains(query)
|
||||
}
|
||||
.sortedBy { it.name.lowercase() }
|
||||
.map { Triple(it.id, it.name.ifBlank { Models.shortId(it.id) }, "squad") }
|
||||
agentItems + memberItems + squadItems
|
||||
}
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
@@ -11114,11 +11279,12 @@ private fun IssueCommentInputBar(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.imePadding()
|
||||
.semantics { contentDescription = "Issue Comment Mention Web Picker" },
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = if (zh) "选择 Agent" else "Mention agent",
|
||||
text = if (zh) "选择 @对象" else "Mention someone",
|
||||
modifier = Modifier.semantics { contentDescription = "Issue Comment Mention Web Header" },
|
||||
style = MaterialTheme.typography.labelMedium.copy(fontSize = 12.sp, lineHeight = 15.sp, fontWeight = FontWeight.SemiBold),
|
||||
color = MulticaColors.Text,
|
||||
@@ -11128,22 +11294,30 @@ private fun IssueCommentInputBar(
|
||||
onValueChange = { mentionQuery = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentDescription = "Issue Comment Mention Search",
|
||||
label = if (zh) "搜索 Agent" else "Search agents",
|
||||
label = if (zh) "搜索人、Agent、小队" else "Search people, agents, squads",
|
||||
singleLine = true,
|
||||
)
|
||||
if (mentionAgents.isEmpty()) {
|
||||
if (mentionItems.isEmpty()) {
|
||||
Text(
|
||||
text = if (zh) "没有可提及的 Agent" else "No agents available",
|
||||
text = if (zh) "没有可提及的对象" else "No mentionable items",
|
||||
style = MaterialTheme.typography.bodySmall.copy(fontSize = 12.sp, lineHeight = 15.sp),
|
||||
color = MulticaColors.Muted,
|
||||
)
|
||||
} else {
|
||||
mentionAgents.forEach { agent ->
|
||||
IssueMentionAgentWebRow(
|
||||
agent = agent,
|
||||
contentDescription = "Issue Comment Mention Agent Row ${agent.id}",
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 240.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
mentionItems.forEach { (id, name, type) ->
|
||||
IssueMentionUnifiedRow(
|
||||
name = name,
|
||||
mentionType = type,
|
||||
contentDescription = "Issue Comment Mention $type Row $id",
|
||||
onClick = {
|
||||
val insertion = AgentMentionMarkdown.displayText(agent.name, agent.id)
|
||||
val insertion = AgentMentionMarkdown.displayText(name, id)
|
||||
val separator = if (draft.isBlank() || draft.endsWith(" ") || draft.endsWith("\n")) "" else " "
|
||||
onDraftChange(draft + separator + insertion + " ")
|
||||
mentionPickerOpen = false
|
||||
@@ -11157,6 +11331,7 @@ private fun IssueCommentInputBar(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PilotAgentTranscript(
|
||||
@@ -11583,6 +11758,7 @@ private fun MarkdownBlock(
|
||||
markdown: String,
|
||||
members: List<Models.Member> = emptyList(),
|
||||
agents: List<Models.Agent> = emptyList(),
|
||||
squads: List<Models.Squad> = emptyList(),
|
||||
currentUser: Models.User? = null,
|
||||
attachments: List<Models.Attachment> = emptyList(),
|
||||
onIssueIdClick: ((String) -> Unit)? = null,
|
||||
@@ -11591,7 +11767,7 @@ private fun MarkdownBlock(
|
||||
val textColor = MulticaColors.Text.toArgb()
|
||||
val mutedColor = MulticaColors.Muted.toArgb()
|
||||
val borderColor = MulticaColors.Border.toArgb()
|
||||
val linkHandler = remember(members, agents, currentUser, onIssueIdClick, onIssueReferenceClick) {
|
||||
val linkHandler = remember(members, agents, squads, currentUser, onIssueIdClick, onIssueReferenceClick) {
|
||||
object : MarkdownRenderer.LinkHandler {
|
||||
override fun resolveMentionLabel(
|
||||
mentionType: String,
|
||||
@@ -11604,6 +11780,7 @@ private fun MarkdownBlock(
|
||||
fallbackLabel,
|
||||
members,
|
||||
agents,
|
||||
squads,
|
||||
currentUser,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,44 +13,83 @@ import java.util.regex.Pattern;
|
||||
|
||||
final class AgentMentionMarkdown {
|
||||
static String markdown(String name, String agentId) {
|
||||
String cleanName = name == null ? "" : name.trim()
|
||||
.replace("[", "\\[")
|
||||
.replace("]", "\\]");
|
||||
if (cleanName.isEmpty()) cleanName = Models.shortId(agentId);
|
||||
return "[@" + cleanName + "](mention://agent/" + agentId + ")";
|
||||
return mentionMarkdown("agent", name, agentId);
|
||||
}
|
||||
|
||||
static String displayText(String name, String agentId) {
|
||||
static String memberMarkdown(String name, String memberId) {
|
||||
return mentionMarkdown("member", name, memberId);
|
||||
}
|
||||
|
||||
static String squadMarkdown(String name, String squadId) {
|
||||
return mentionMarkdown("squad", name, squadId);
|
||||
}
|
||||
|
||||
static String displayText(String name, String id) {
|
||||
String cleanName = cleanName(name);
|
||||
if (cleanName.isEmpty()) cleanName = Models.shortId(agentId);
|
||||
if (cleanName.isEmpty()) cleanName = Models.shortId(id);
|
||||
return "@" + cleanName;
|
||||
}
|
||||
|
||||
static String markdownFromDraft(String draft, List<Models.Agent> agents) {
|
||||
String content = draft == null ? "" : draft.trim();
|
||||
if (content.isEmpty() || content.contains("mention://agent/") || agents == null || agents.isEmpty()) {
|
||||
return content;
|
||||
return markdownFromDraft(draft, agents, Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
ArrayList<Models.Agent> sortedAgents = new ArrayList<>(agents);
|
||||
sortedAgents.sort((left, right) -> Integer.compare(
|
||||
displayText(right.name, right.id).length(),
|
||||
displayText(left.name, left.id).length()
|
||||
));
|
||||
|
||||
static String markdownFromDraft(String draft, List<Models.Agent> agents,
|
||||
List<Models.Member> members, List<Models.Squad> squads) {
|
||||
String content = draft == null ? "" : draft.trim();
|
||||
if (content.isEmpty() || content.contains("mention://")) return content;
|
||||
ArrayList<MentionReplacement> replacements = new ArrayList<>();
|
||||
if (agents != null) {
|
||||
for (Models.Agent agent : agents) {
|
||||
replacements.add(new MentionReplacement(displayText(agent.name, agent.id), markdown(agent.name, agent.id)));
|
||||
}
|
||||
}
|
||||
if (members != null) {
|
||||
for (Models.Member member : members) {
|
||||
replacements.add(new MentionReplacement(displayText(member.displayName, member.id), memberMarkdown(member.displayName, member.id)));
|
||||
if (member.userId != null && !member.userId.isEmpty() && !member.userId.equals(member.id)) {
|
||||
replacements.add(new MentionReplacement(displayText(member.displayName, member.userId), memberMarkdown(member.displayName, member.userId)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (squads != null) {
|
||||
for (Models.Squad squad : squads) {
|
||||
replacements.add(new MentionReplacement(displayText(squad.name, squad.id), squadMarkdown(squad.name, squad.id)));
|
||||
}
|
||||
}
|
||||
replacements.sort((left, right) -> Integer.compare(right.display.length(), left.display.length()));
|
||||
String normalized = content;
|
||||
for (Models.Agent agent : sortedAgents) {
|
||||
String display = displayText(agent.name, agent.id);
|
||||
if (display.length() <= 1) continue;
|
||||
normalized = normalized.replace(display, markdown(agent.name, agent.id));
|
||||
for (MentionReplacement replacement : replacements) {
|
||||
if (replacement.display.length() <= 1) continue;
|
||||
normalized = normalized.replace(replacement.display, replacement.markdown);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static String mentionMarkdown(String type, String name, String id) {
|
||||
String cleanName = cleanName(name)
|
||||
.replace("[", "\\[")
|
||||
.replace("]", "\\]");
|
||||
if (cleanName.isEmpty()) cleanName = Models.shortId(id);
|
||||
return "[@" + cleanName + "](mention://" + type + "/" + id + ")";
|
||||
}
|
||||
|
||||
private static String cleanName(String name) {
|
||||
if (name == null) return "";
|
||||
String trimmed = name.trim();
|
||||
return "null".equalsIgnoreCase(trimmed) ? "" : trimmed;
|
||||
}
|
||||
|
||||
private static final class MentionReplacement {
|
||||
final String display;
|
||||
final String markdown;
|
||||
|
||||
MentionReplacement(String display, String markdown) {
|
||||
this.display = display;
|
||||
this.markdown = markdown;
|
||||
}
|
||||
}
|
||||
|
||||
private AgentMentionMarkdown() {
|
||||
}
|
||||
}
|
||||
@@ -60,13 +99,19 @@ final class IssueCommentRichText {
|
||||
|
||||
static String mentionDisplayLabel(String mentionType, String mentionId, String fallbackLabel,
|
||||
List<Models.Member> members, List<Models.Agent> agents,
|
||||
Models.User currentUser) {
|
||||
String resolved = resolveMentionName(mentionType, mentionId, members, agents, currentUser);
|
||||
List<Models.Squad> squads, Models.User currentUser) {
|
||||
String resolved = resolveMentionName(mentionType, mentionId, members, agents, squads, currentUser);
|
||||
String label = resolved.isEmpty() ? cleanMentionFallback(fallbackLabel, mentionId) : resolved;
|
||||
if (label.startsWith("@")) return label;
|
||||
return "@" + label;
|
||||
}
|
||||
|
||||
static String mentionDisplayLabel(String mentionType, String mentionId, String fallbackLabel,
|
||||
List<Models.Member> members, List<Models.Agent> agents,
|
||||
Models.User currentUser) {
|
||||
return mentionDisplayLabel(mentionType, mentionId, fallbackLabel, members, agents, Collections.emptyList(), currentUser);
|
||||
}
|
||||
|
||||
static int issueIdentifierEndAt(String value, int start) {
|
||||
if (value == null || start < 0 || start >= value.length()) return -1;
|
||||
Matcher matcher = ISSUE_IDENTIFIER.matcher(value);
|
||||
@@ -105,7 +150,7 @@ final class IssueCommentRichText {
|
||||
|
||||
private static String resolveMentionName(String mentionType, String mentionId,
|
||||
List<Models.Member> members, List<Models.Agent> agents,
|
||||
Models.User currentUser) {
|
||||
List<Models.Squad> squads, Models.User currentUser) {
|
||||
String type = mentionType == null ? "" : mentionType.toLowerCase(Locale.US);
|
||||
String id = mentionId == null ? "" : mentionId;
|
||||
if ("agent".equals(type)) {
|
||||
@@ -119,6 +164,11 @@ final class IssueCommentRichText {
|
||||
if (id.equals(member.id) || id.equals(member.userId)) return clean(member.displayName);
|
||||
}
|
||||
}
|
||||
if ("squad".equals(type)) {
|
||||
for (Models.Squad squad : squads) {
|
||||
if (id.equals(squad.id)) return clean(squad.name);
|
||||
}
|
||||
}
|
||||
for (Models.Agent agent : agents) {
|
||||
if (id.equals(agent.id)) return clean(agent.name);
|
||||
}
|
||||
@@ -126,6 +176,9 @@ final class IssueCommentRichText {
|
||||
for (Models.Member member : members) {
|
||||
if (id.equals(member.id) || id.equals(member.userId)) return clean(member.displayName);
|
||||
}
|
||||
for (Models.Squad squad : squads) {
|
||||
if (id.equals(squad.id)) return clean(squad.name);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ public final class MainActivity extends Activity {
|
||||
private List<Models.Project> projectCache = new ArrayList<>();
|
||||
private List<Models.Member> memberCache = new ArrayList<>();
|
||||
private List<Models.Agent> agentCache = new ArrayList<>();
|
||||
private List<Models.Squad> squadCache = new ArrayList<>();
|
||||
private List<Models.Runtime> runtimeCache = new ArrayList<>();
|
||||
private List<Models.Skill> skillCache = new ArrayList<>();
|
||||
private List<Models.Autopilot> autopilotCache = new ArrayList<>();
|
||||
@@ -266,6 +267,7 @@ public final class MainActivity extends Activity {
|
||||
projectCache = new ArrayList<>();
|
||||
memberCache = new ArrayList<>();
|
||||
agentCache = new ArrayList<>();
|
||||
squadCache = new ArrayList<>();
|
||||
runtimeCache = new ArrayList<>();
|
||||
skillCache = new ArrayList<>();
|
||||
autopilotCache = new ArrayList<>();
|
||||
@@ -565,6 +567,7 @@ public final class MainActivity extends Activity {
|
||||
projectCache = data.projects;
|
||||
memberCache = data.members;
|
||||
agentCache = data.agents;
|
||||
try { squadCache = safeSquads(); } catch (Exception ignored) { squadCache = new ArrayList<>(); }
|
||||
LinearLayout screen = vertical();
|
||||
LinearLayout scrollBody = vertical();
|
||||
scrollBody.setPadding(dp(14), dp(8), dp(14), dp(14));
|
||||
@@ -1833,6 +1836,10 @@ public final class MainActivity extends Activity {
|
||||
}
|
||||
}
|
||||
|
||||
private List<Models.Squad> safeSquads() {
|
||||
try { return api.squads(currentWorkspace.id); } catch (Exception e) { return new ArrayList<>(); }
|
||||
}
|
||||
|
||||
private List<Models.Agent> safeAgents() {
|
||||
if (currentWorkspace == null) return Models.emptyList();
|
||||
try {
|
||||
@@ -1854,7 +1861,7 @@ public final class MainActivity extends Activity {
|
||||
private List<Models.Assignee> assignees(boolean includeEmpty) {
|
||||
if (memberCache.isEmpty()) memberCache = safeMembers();
|
||||
if (agentCache.isEmpty()) agentCache = safeAgents();
|
||||
return Models.issueAssignees(includeEmpty, t("unassigned"), currentUser, memberCache, agentCache);
|
||||
return Models.issueAssignees(includeEmpty, t("unassigned"), currentUser, memberCache, agentCache, squadCache);
|
||||
}
|
||||
|
||||
private String assigneeName(String id, String type) {
|
||||
@@ -1862,7 +1869,10 @@ public final class MainActivity extends Activity {
|
||||
if (currentUser != null && currentUser.id.equals(id)) return currentUser.name;
|
||||
for (Models.Member member : memberCache) if (member.id.equals(id)) return member.displayName;
|
||||
for (Models.Agent agent : agentCache) if (agent.id.equals(id)) return agent.name;
|
||||
String prefix = "agent".equals(type) ? t("agent") : t("member");
|
||||
String prefix;
|
||||
if ("agent".equals(type)) prefix = t("agent");
|
||||
else if ("squad".equals(type)) prefix = t("squad");
|
||||
else prefix = t("member");
|
||||
return prefix + " " + Models.shortId(id);
|
||||
}
|
||||
|
||||
@@ -2252,6 +2262,7 @@ public final class MainActivity extends Activity {
|
||||
case "unassigned": return "未分配";
|
||||
case "member": return "成员";
|
||||
case "agent": return "Agent";
|
||||
case "squad": return "小队";
|
||||
case "archive": return "归档";
|
||||
case "archived": return "已归档";
|
||||
case "archiveAgent": return "归档 Agent";
|
||||
@@ -2364,6 +2375,7 @@ public final class MainActivity extends Activity {
|
||||
case "unassigned": return "Unassigned";
|
||||
case "member": return "Member";
|
||||
case "agent": return "Agent";
|
||||
case "squad": return "Squad";
|
||||
case "archive": return "Archive";
|
||||
case "archived": return "Archived";
|
||||
case "archiveAgent": return "Archive Agent";
|
||||
|
||||
@@ -166,13 +166,17 @@ final class Models {
|
||||
return subscriber.userType == null ? "" : subscriber.userType;
|
||||
}
|
||||
|
||||
static String issueAssigneeDisplayName(Issue issue, List<Member> members, List<Agent> agents, User currentUser) {
|
||||
static String issueAssigneeDisplayName(Issue issue, List<Member> members, List<Agent> agents, List<Squad> squads, User currentUser) {
|
||||
if (issue == null || issue.assigneeId == null || issue.assigneeId.isEmpty()) return "";
|
||||
String assigneeId = issue.assigneeId;
|
||||
if ("agent".equals(issue.assigneeType)) {
|
||||
for (Agent agent : agents) {
|
||||
if (assigneeId.equals(agent.id)) return agent.name;
|
||||
}
|
||||
} else if ("squad".equals(issue.assigneeType)) {
|
||||
for (Squad squad : squads) {
|
||||
if (assigneeId.equals(squad.id)) return squad.name;
|
||||
}
|
||||
} else {
|
||||
if (currentUser != null && assigneeId.equals(currentUser.id)) return currentUser.name;
|
||||
for (Member member : members) {
|
||||
@@ -185,11 +189,16 @@ final class Models {
|
||||
for (Member member : members) {
|
||||
if (assigneeId.equals(member.id) || assigneeId.equals(member.userId)) return member.displayName;
|
||||
}
|
||||
for (Squad squad : squads) {
|
||||
if (assigneeId.equals(squad.id)) return squad.name;
|
||||
}
|
||||
String prefix;
|
||||
if ("agent".equals(issue.assigneeType)) {
|
||||
prefix = "Agent ";
|
||||
} else if ("member".equals(issue.assigneeType)) {
|
||||
prefix = "Member ";
|
||||
} else if ("squad".equals(issue.assigneeType)) {
|
||||
prefix = "Squad ";
|
||||
} else {
|
||||
prefix = "";
|
||||
}
|
||||
@@ -197,7 +206,7 @@ final class Models {
|
||||
}
|
||||
|
||||
static List<Assignee> issueAssignees(boolean includeEmpty, String emptyLabel, User currentUser,
|
||||
List<Member> members, List<Agent> agents) {
|
||||
List<Member> members, List<Agent> agents, List<Squad> squads) {
|
||||
List<Assignee> list = new ArrayList<>();
|
||||
List<String> seenMemberIds = new ArrayList<>();
|
||||
if (includeEmpty) list.add(new Assignee(null, null, emptyLabel));
|
||||
@@ -214,6 +223,10 @@ final class Models {
|
||||
if (agent.id == null || agent.id.isEmpty()) continue;
|
||||
list.add(new Assignee(agent.id, "agent", agent.name));
|
||||
}
|
||||
for (Squad squad : squads) {
|
||||
if (squad.id == null || squad.id.isEmpty()) continue;
|
||||
list.add(new Assignee(squad.id, "squad", squad.name));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -756,6 +769,27 @@ final class Models {
|
||||
}
|
||||
}
|
||||
|
||||
static final class Squad {
|
||||
final String id;
|
||||
final String name;
|
||||
final String description;
|
||||
final String avatarUrl;
|
||||
final String archivedAt;
|
||||
final List<String> agentIds;
|
||||
final List<String> memberIds;
|
||||
|
||||
Squad(JSONObject json) {
|
||||
id = json.optString("id");
|
||||
name = json.optString("name", shortId(id));
|
||||
description = json.optString("description", "");
|
||||
avatarUrl = cleanString(json, "avatar_url", cleanString(json, "avatarUrl", ""));
|
||||
archivedAt = cleanString(json, "archived_at", "");
|
||||
agentIds = stringList(json.optJSONArray("agent_ids"));
|
||||
memberIds = stringList(json.optJSONArray("member_ids"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static final class Runtime {
|
||||
final String id;
|
||||
final String workspaceId;
|
||||
|
||||
Reference in New Issue
Block a user