Add squad mentions and assignee support

This commit is contained in:
Xisheng-Zhao
2026-06-07 04:08:17 +08:00
parent 3f720b72cf
commit 4ae42b14d3
5 changed files with 363 additions and 75 deletions
@@ -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";
+36 -2
View File
@@ -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;