mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-10 13:10:14 +00:00
* [MM-68856] Respect default agent and persist last-selected agent in agent selectors
Mobile agent selectors always defaulted to the first agent and did not
remember the user's previous choice, ignoring the system-wide default
agent configured on the server.
Selectors now resolve the active agent with the precedence
saved-preference -> system default (is_default) -> first, and persist
explicit selections via the shared `agents/selected_agent` core
preference so the choice restores across surfaces and clients.
Applied to the rewrite options, channel summary, and agent chat
selectors. Part of a coordinated 3-repo change (webapp,
mattermost-plugin-agents, mobile) sharing the same preference contract.
Co-authored-by: Cursor <cursoragent@cursor.com>
* [MM-68856] Address review feedback on agent selector
- await savePreference in saveSelectedAgent so its errors stay inside the
try/catch and callers get a consistent {error} result.
- Surface persistence failures from the resolved {error} on all three
selector surfaces (agent chat, channel summary, rewrite options) via
logError instead of dropping them silently.
- Rename agent selection tests to the `should...` convention.
Co-authored-by: Cursor <cursoragent@cursor.com>
* [MM-68856] Address review feedback on agent selector structure
Split channel summary and rewrite options HOCs into index files, switch
persist calls to async/await, and cover non-empty selectedAgentId on mount.
Co-authored-by: Cursor <cursoragent@cursor.com>
* [MM-68856] Log agent selection failures with getFullErrorMessage
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
121 lines
3.7 KiB
TypeScript
121 lines
3.7 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {AGENT_POST_TYPES} from '@agents/constants';
|
|
import {ToolApprovalStage, ToolCallStatus, type ToolCall} from '@agents/types';
|
|
|
|
import type PostModel from '@typings/database/models/servers/post';
|
|
|
|
/**
|
|
* Resolve which agent/bot should be selected when a selector opens.
|
|
* Precedence: saved preference (if still available) -> system default -> first.
|
|
* `is_default` is tolerated when absent (older servers / DB-backed lists).
|
|
*/
|
|
export function resolveSelectedAgent<T extends {id: string; is_default?: boolean}>(agents: T[], savedPrefId?: string | null): T | null {
|
|
if (agents.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
if (savedPrefId) {
|
|
const saved = agents.find((a) => a.id === savedPrefId);
|
|
if (saved) {
|
|
return saved;
|
|
}
|
|
}
|
|
|
|
return agents.find((a) => a.is_default) ?? agents[0];
|
|
}
|
|
|
|
/**
|
|
* Check if a post is an agent post
|
|
*/
|
|
export function isAgentPost(post: PostModel | Post): boolean {
|
|
return post.type === AGENT_POST_TYPES.LLMBOT ||
|
|
post.type === AGENT_POST_TYPES.LLM_POSTBACK;
|
|
}
|
|
|
|
/**
|
|
* Check if the current user is the requester of an agent post
|
|
* @param post The agent post
|
|
* @param currentUserId The current user ID
|
|
* @returns true if current user is the requester
|
|
*/
|
|
export function isPostRequester(post: PostModel | Post, currentUserId: string): boolean {
|
|
try {
|
|
const props = post.props as Record<string, unknown>;
|
|
return props?.llm_requester_user_id === currentUserId;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a post has redacted tool call data (private arguments hidden from channel)
|
|
*/
|
|
export function isToolCallRedacted(post: PostModel | Post): boolean {
|
|
try {
|
|
const props = post.props as Record<string, unknown>;
|
|
return props?.pending_tool_call_redacted === 'true';
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a post is pending tool result approval
|
|
*/
|
|
export function isPendingToolResult(post: PostModel | Post): boolean {
|
|
try {
|
|
const props = post.props as Record<string, unknown>;
|
|
return props?.pending_tool_result === 'true';
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine the current tool approval stage for a post. Returns Done when
|
|
* there is no outstanding user decision (no pending tools and no redacted
|
|
* result flag).
|
|
*/
|
|
export function getToolApprovalStage(post: PostModel | Post, toolCalls: ToolCall[]): ToolApprovalStage {
|
|
if (isPendingToolResult(post)) {
|
|
return ToolApprovalStage.Result;
|
|
}
|
|
if (toolCalls.some((tc) => tc.status === ToolCallStatus.Pending)) {
|
|
return ToolApprovalStage.Call;
|
|
}
|
|
return ToolApprovalStage.Done;
|
|
}
|
|
|
|
/**
|
|
* Merge public tool calls with private data, preserving status from public and arguments/results from private
|
|
*/
|
|
export function mergeToolCalls(publicCalls: ToolCall[], privateCalls: ToolCall[] | null): ToolCall[] {
|
|
if (!privateCalls?.length) {
|
|
return publicCalls;
|
|
}
|
|
|
|
const privateById = new Map(privateCalls.map((tc) => [tc.id, tc]));
|
|
|
|
const merged = publicCalls.map((publicTool) => {
|
|
const privateTool = privateById.get(publicTool.id);
|
|
if (!privateTool) {
|
|
return publicTool;
|
|
}
|
|
privateById.delete(publicTool.id);
|
|
return {
|
|
...publicTool,
|
|
arguments: privateTool.arguments,
|
|
...(privateTool.result != null && {result: privateTool.result}),
|
|
};
|
|
});
|
|
|
|
// Append any private-only tools not found in public calls
|
|
for (const privateTool of privateById.values()) {
|
|
merged.push(privateTool);
|
|
}
|
|
|
|
return merged;
|
|
}
|