codexmobile-relay / client /src /app-message-state.js
Codex
deploy: CodexMobile Relay
90f0300
Raw
History Blame Contribute Delete
10.7 kB
import {
activityStepFromPayload,
briefActivityLabel,
isGenericActivityLabel,
statusMessageId
} from './app-activity-labels.js';
import { payloadRunKeys } from './app-core-utils.js';
export function mergeActivityStep(currentSteps, step) {
if (!step) {
return currentSteps || [];
}
const steps = [...(currentSteps || [])];
const existingIndex = steps.findIndex((item) => item.id === step.id);
if (existingIndex >= 0) {
steps[existingIndex] = { ...steps[existingIndex], ...step };
return steps.slice(-8);
}
const sameWorkIndex = steps.findIndex(
(item) =>
item.kind === step.kind &&
item.label === step.label &&
(item.command || '') === (step.command || '')
);
if (sameWorkIndex >= 0) {
steps[sameWorkIndex] = { ...steps[sameWorkIndex], ...step };
return steps.slice(-8);
}
const last = steps[steps.length - 1];
if (last && last.label === step.label && last.detail === step.detail && last.status === step.status) {
return steps;
}
return [...steps, step].slice(-8);
}
export function isVisibleActivityStep(step, messageStatus) {
if (!step) {
return false;
}
const label = String(step.label || '').trim();
if (isGenericActivityLabel(label)) {
return false;
}
if (
['reasoning', 'message', 'agent_message'].includes(step.kind) &&
/^(正在思考中?|正在处理|正在回复|正在整理回复)$/.test(label)
) {
return false;
}
if (messageStatus !== 'failed' && step.kind === 'command_execution' && step.status === 'failed') {
return false;
}
if (messageStatus !== 'failed' && /blocked by policy|rejected/i.test(`${step.detail || ''}\n${step.output || ''}\n${step.error || ''}`)) {
return false;
}
return true;
}
export function upsertStatusMessage(current, payload) {
const id = statusMessageId(payload);
const existingIndex = current.findIndex((message) => message.id === id);
const previous = existingIndex >= 0 ? current[existingIndex] : null;
const normalizedPayload =
payload.kind === 'agent_message'
? { ...payload, label: briefActivityLabel(payload.label || payload.content) }
: payload;
const detail =
normalizedPayload.kind === 'reasoning'
? previous?.detail || ''
: normalizedPayload.detail || previous?.detail || '';
const status = normalizedPayload.status || previous?.status || 'running';
const isTerminalStatus = ['aborted', 'completed', 'failed'].includes(status);
const kind = normalizedPayload.kind || (isTerminalStatus ? 'turn' : previous?.kind || 'turn');
const isTurnLevel = kind === 'turn' || kind === 'error';
const nextMessage = {
id,
role: 'activity',
turnId: normalizedPayload.turnId || previous?.turnId || null,
sessionId: normalizedPayload.sessionId || previous?.sessionId || null,
content: isTurnLevel ? (normalizedPayload.label || previous?.content || '正在处理') : (previous?.content || '正在处理'),
label: isTurnLevel ? (normalizedPayload.label || previous?.label || '正在处理') : (previous?.label || '正在处理'),
detail,
kind,
status: isTurnLevel ? status : (previous?.status || 'running'),
timestamp: normalizedPayload.timestamp || previous?.timestamp || new Date().toISOString(),
activities: mergeActivityStep(previous?.activities || [], activityStepFromPayload(normalizedPayload))
};
if (existingIndex >= 0) {
const next = [...current];
next[existingIndex] = nextMessage;
return next;
}
return [...current, nextMessage];
}
export function upsertActivityMessage(current, payload) {
const id = statusMessageId(payload);
const existingIndex = current.findIndex((message) => message.id === id);
const previous = existingIndex >= 0 ? current[existingIndex] : null;
const isTurnLevel = payload.kind === 'turn' || payload.kind === 'error';
const activity = activityStepFromPayload(payload, 'activity');
if (!activity && !previous) {
return current;
}
const activities = activity
? mergeActivityStep(previous?.activities || [], activity)
: previous?.activities || [];
const nextMessage = {
id,
role: 'activity',
turnId: payload.turnId || previous?.turnId || null,
sessionId: payload.sessionId || previous?.sessionId || null,
content: previous?.content || '正在处理',
label: previous?.label || '正在处理',
detail: payload.detail || previous?.detail || activity?.detail || '',
kind: payload.kind || previous?.kind || 'activity',
status: isTurnLevel ? (payload.status || previous?.status || 'running') : (previous?.status || 'running'),
timestamp: previous?.timestamp || payload.timestamp || new Date().toISOString(),
activities
};
if (existingIndex >= 0) {
const next = [...current];
next[existingIndex] = nextMessage;
return next;
}
return [...current, nextMessage];
}
export function completeStatusMessage(current, payload) {
const id = statusMessageId(payload);
return current.filter((message) => message.id !== id);
}
export function hasAssistantMessageForTurn(messages, payload) {
return messages.some(
(message) =>
message.role === 'assistant' &&
payload?.turnId &&
message.turnId === payload.turnId &&
typeof message.content === 'string' &&
message.content.trim()
);
}
export function removeActivityMessagesForTurn(messages, payload) {
const keys = new Set(payloadRunKeys(payload));
if (!keys.size) {
return messages;
}
return messages.filter((message) => {
if (message.role !== 'activity') {
return true;
}
return !payloadRunKeys(message).some((key) => keys.has(key));
});
}
function normalizedContent(message) {
return String(message?.content || '').trim();
}
function contentKey(message) {
return `${message?.role || ''}\n${normalizedContent(message)}`;
}
function countServerContent(messages) {
const counts = new Map();
for (const message of messages) {
const key = contentKey(message);
if (!normalizedContent(message)) {
continue;
}
counts.set(key, (counts.get(key) || 0) + 1);
}
return counts;
}
function isPendingLocalMessage(message) {
return (
message?.role === 'activity' ||
message?.preview ||
String(message?.id || '').startsWith('local-')
);
}
function countPendingLocalContent(messages) {
const counts = new Map();
for (const message of messages || []) {
if (!isPendingLocalMessage(message) || !normalizedContent(message)) {
continue;
}
const key = contentKey(message);
counts.set(key, (counts.get(key) || 0) + 1);
}
return counts;
}
function consumeContentMatch(counts, message) {
const key = contentKey(message);
const count = counts.get(key) || 0;
if (!count) {
return false;
}
if (count === 1) {
counts.delete(key);
} else {
counts.set(key, count - 1);
}
return true;
}
function turnScopedRunKeys(payload) {
return [payload?.turnId, payload?.previousSessionId].filter(Boolean);
}
export function mergeServerMessagesWithLocalState(current, serverMessages, options = {}) {
const next = Array.isArray(serverMessages) ? [...serverMessages] : [];
const activeKeys = new Set((options.activeRuns || []).flatMap((run) => payloadRunKeys(run)));
const serverIds = new Set(next.map((message) => message.id).filter(Boolean));
const serverKeysByRole = new Map();
for (const message of next) {
for (const key of turnScopedRunKeys(message)) {
const role = message.role || '';
const roleKeys = serverKeysByRole.get(role) || new Set();
roleKeys.add(key);
serverKeysByRole.set(role, roleKeys);
}
}
const serverContentCounts = countServerContent(next);
const pendingLocalContentCounts = countPendingLocalContent(current);
const shouldPreserveLocalRun = (message) => {
if (options.preserveLocalRuns) {
return true;
}
const keys = message.turnId ? [message.turnId] : payloadRunKeys(message);
return keys.some((key) => activeKeys.has(key));
};
const serverHasRoleKey = (message) => {
const roleKeys = serverKeysByRole.get(message.role || '');
const keys = turnScopedRunKeys(message);
return Boolean(keys.length && keys.some((key) => roleKeys?.has(key)));
};
const serverHasAssistantForRun = (message) => {
const roleKeys = serverKeysByRole.get('assistant');
const keys = turnScopedRunKeys(message);
return Boolean(keys.length && keys.some((key) => roleKeys?.has(key)));
};
const hasServerMessageForLocal = (message) => {
if (serverIds.has(message.id)) {
return true;
}
if (message.role === 'activity' || message.preview) {
return serverHasAssistantForRun(message);
}
if (serverHasRoleKey(message)) {
return true;
}
if ((pendingLocalContentCounts.get(contentKey(message)) || 0) > 1) {
return false;
}
return consumeContentMatch(serverContentCounts, message);
};
for (const message of current || []) {
if (message.transient) {
continue;
}
if (!isPendingLocalMessage(message)) {
continue;
}
if (!shouldPreserveLocalRun(message)) {
continue;
}
if (hasServerMessageForLocal(message)) {
continue;
}
next.push(message);
}
return next;
}
export function upsertAssistantMessage(current, payload) {
const content = String(payload.content || '').trim();
if (!content) {
return current;
}
const id = payload.messageId || `assistant-${payload.turnId || Date.now()}`;
const isPreview = Boolean(payload.preview);
const nextMessage = {
id,
role: 'assistant',
content,
timestamp: new Date().toISOString(),
turnId: payload.turnId || null,
sessionId: payload.sessionId || null,
kind: payload.kind,
preview: isPreview || undefined
};
const withoutActivity = removeActivityMessagesForTurn(current, payload);
const withoutStalePreview =
!isPreview && payload.turnId
? withoutActivity.filter(
(message) => !(message.role === 'assistant' && message.preview && message.turnId === payload.turnId)
)
: withoutActivity;
const withoutDuplicateTurnAssistant =
!isPreview && payload.turnId
? withoutStalePreview.filter(
(message) => !(message.role === 'assistant' && message.turnId === payload.turnId && message.id !== id)
)
: withoutStalePreview;
const existingIndex = withoutDuplicateTurnAssistant.findIndex((message) => message.id === id);
if (existingIndex >= 0) {
const next = [...withoutDuplicateTurnAssistant];
next[existingIndex] = nextMessage;
return next;
}
return [...withoutDuplicateTurnAssistant, nextMessage];
}