Merge pull request #1303 from pikasTech/fix-1302-session-list-updated-time

fix: session tab time follows latest activity
This commit is contained in:
Lyon
2026-06-16 04:10:48 +08:00
committed by GitHub
3 changed files with 92 additions and 1 deletions
@@ -46,6 +46,22 @@ test("R2 session tabs expose label, model metadata source, count and active stat
assert.match(tabs[0]?.subtitle ?? "", /trace trc_done/u); assert.match(tabs[0]?.subtitle ?? "", /trace trc_done/u);
}); });
test("R2 session tabs use latest message activity time instead of stale conversation time", () => {
const tabs = sortSessionTabs([
{
conversationId: "cnv_old_created_recent_message",
sessionId: "ses_old_created_recent_message",
firstUserMessagePreview: "三天前创建但刚刚更新",
updatedAt: "2026-01-01T00:00:00.000Z",
startedAt: "2026-01-01T00:00:00.000Z",
messages: [agentMessage({ traceId: "trc_recent", conversationId: "cnv_old_created_recent_message", updatedAt: "2026-01-04T00:01:00.000Z" })]
},
{ conversationId: "cnv_newer_created", sessionId: "ses_newer_created", firstUserMessagePreview: "创建时间较新但未更新", updatedAt: "2026-01-03T00:00:00.000Z" }
], null);
assert.equal(tabs[0]?.conversationId, "cnv_old_created_recent_message");
assert.equal(tabs[0]?.updatedAt, "2026-01-04T00:01:00.000Z");
});
test("R2 session tab labels normalize object previews and reject object placeholders", () => { test("R2 session tab labels normalize object previews and reject object placeholders", () => {
const tabs = sortSessionTabs([ const tabs = sortSessionTabs([
{ conversationId: "cnv_object", sessionId: "ses_object", firstUserMessagePreview: { text: "对象预览标题" } as unknown as string, updatedAt: "2026-01-03T00:00:00.000Z" }, { conversationId: "cnv_object", sessionId: "ses_object", firstUserMessagePreview: { text: "对象预览标题" } as unknown as string, updatedAt: "2026-01-03T00:00:00.000Z" },
@@ -90,7 +90,7 @@ export function workspaceWithClearedActiveTrace(workspace: WorkspaceRecord | nul
export function conversationToSessionTab(conversation: ConversationRecord, activeConversationId: string | null): SessionTab { export function conversationToSessionTab(conversation: ConversationRecord, activeConversationId: string | null): SessionTab {
const sessionId = firstNonEmptyString(conversation.sessionId, conversation.session?.sessionId); const sessionId = firstNonEmptyString(conversation.sessionId, conversation.session?.sessionId);
const conversationId = conversation.conversationId; const conversationId = conversation.conversationId;
const updatedAt = firstNonEmptyString(conversation.updatedAt, conversation.snapshot?.updatedAt, conversation.startedAt); const updatedAt = conversationActivityUpdatedAt(conversation);
const status = firstNonEmptyString(conversation.snapshot?.sessionStatus, conversation.status, conversation.messages?.at(-1)?.status) ?? "source"; const status = firstNonEmptyString(conversation.snapshot?.sessionStatus, conversation.status, conversation.messages?.at(-1)?.status) ?? "source";
const trace = firstNonEmptyString(conversation.lastTraceId, conversation.messages?.at(-1)?.traceId); const trace = firstNonEmptyString(conversation.lastTraceId, conversation.messages?.at(-1)?.traceId);
const userMessage = conversation.messages?.find((message) => message.role === "user"); const userMessage = conversation.messages?.find((message) => message.role === "user");
@@ -223,6 +223,32 @@ function firstReadableSentence(...values: unknown[]): string | null {
return null; return null;
} }
function conversationActivityUpdatedAt(conversation: ConversationRecord): string | null {
const messageTimes = (conversation.messages ?? []).flatMap((message) => [
message.updatedAt,
message.runnerTrace?.updatedAt,
message.createdAt
]);
return latestTimestamp(
conversation.updatedAt,
conversation.snapshot?.updatedAt,
...messageTimes,
conversation.startedAt
);
}
function latestTimestamp(...values: unknown[]): string | null {
let latest: { value: string; ms: number } | null = null;
for (const value of values) {
const text = firstNonEmptyString(value);
if (!text) continue;
const ms = timestampMs(text);
if (!ms) continue;
if (!latest || ms > latest.ms) latest = { value: text, ms };
}
return latest?.value ?? firstNonEmptyString(...values) ?? null;
}
function readableSentence(value: unknown): string | null { function readableSentence(value: unknown): string | null {
if (typeof value === "string") return firstSentenceFromText(value); if (typeof value === "string") return firstSentenceFromText(value);
if (typeof value === "number" || typeof value === "boolean") return String(value); if (typeof value === "number" || typeof value === "boolean") return String(value);
@@ -130,6 +130,43 @@ export const useWorkbenchStore = defineStore("workbench", () => {
if (response.ok) conversations.value = response.data?.conversations ?? []; if (response.ok) conversations.value = response.data?.conversations ?? [];
} }
function bumpConversationActivity(input: { conversationId?: string | null; sessionId?: string | null; threadId?: string | null; traceId?: string | null; status?: string | null; updatedAt?: string | null }): void {
const traceMessage = input.traceId ? messages.value.find((message) => message.traceId === input.traceId) : null;
const conversationId = firstNonEmptyString(input.conversationId, traceMessage?.conversationId, activeConversationId.value);
if (!conversationId) return;
const existing = conversations.value.find((conversation) => conversation.conversationId === conversationId) ?? null;
const activityMessages = activeConversationId.value === conversationId ? messages.value : messages.value.filter((message) => message.conversationId === conversationId);
const latestAgent = [...activityMessages].reverse().find((message) => message.role === "agent");
const latestTrace = [...activityMessages].reverse().find((message) => message.traceId);
const userMessage = activityMessages.find((message) => message.role === "user");
const updatedAt = firstNonEmptyString(input.updatedAt, traceMessage?.updatedAt, latestAgent?.updatedAt, latestAgent?.runnerTrace?.updatedAt, latestTrace?.updatedAt, new Date().toISOString()) ?? new Date().toISOString();
const status = firstNonEmptyString(input.status, latestAgent?.status, existing?.status) ?? existing?.status ?? null;
const sessionId = firstNonEmptyString(input.sessionId, traceMessage?.sessionId, latestAgent?.sessionId, existing?.sessionId, existing?.session?.sessionId, selectedSessionId.value) ?? null;
const threadId = firstNonEmptyString(input.threadId, traceMessage?.threadId, latestAgent?.threadId, existing?.threadId, existing?.session?.threadId, selectedThreadId.value) ?? null;
const lastTraceId = firstNonEmptyString(input.traceId, latestTrace?.traceId, existing?.lastTraceId) ?? null;
const nextMessages = activityMessages.length > 0 ? activityMessages : existing?.messages ?? [];
const firstUserMessagePreview = firstNonEmptyString(existing?.firstUserMessagePreview, existing?.snapshot?.firstUserMessagePreview, userMessage?.text) ?? null;
const next: ConversationRecord = {
...(existing ?? { conversationId }),
conversationId,
projectId: existing?.projectId ?? activeProjectId.value,
sessionId,
threadId,
status,
lastTraceId,
updatedAt,
messages: nextMessages,
messageCount: nextMessages.length || existing?.messageCount,
firstUserMessagePreview,
snapshot: {
...(existing?.snapshot ?? {}),
...(status ? { sessionStatus: status } : {}),
updatedAt
}
};
conversations.value = [next, ...conversations.value.filter((conversation) => conversation.conversationId !== conversationId)];
}
async function submitMessage(text: string): Promise<void> { async function submitMessage(text: string): Promise<void> {
const value = text.trim(); const value = text.trim();
if (!value) return; if (!value) return;
@@ -146,6 +183,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
const user = makeMessage("user", value, "sent", { traceId: steerTraceId ?? traceId, conversationId, sessionId, threadId, title: steerMode ? "用户引导" : "用户" }); const user = makeMessage("user", value, "sent", { traceId: steerTraceId ?? traceId, conversationId, sessionId, threadId, title: steerMode ? "用户引导" : "用户" });
const pending = makeMessage("agent", "", "running", { traceId, conversationId, sessionId, threadId, title: "Code Agent", retryInput: value }); const pending = makeMessage("agent", "", "running", { traceId, conversationId, sessionId, threadId, title: "Code Agent", retryInput: value });
messages.value.push(user, pending); messages.value.push(user, pending);
bumpConversationActivity({ conversationId, sessionId, threadId, traceId, status: "running" });
chatPending.value = true; chatPending.value = true;
currentRequest.value = { traceId, conversationId, sessionId, threadId, status: "running" }; currentRequest.value = { traceId, conversationId, sessionId, threadId, status: "running" };
rememberRecentDraft(value); rememberRecentDraft(value);
@@ -284,6 +322,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
function applyTraceSnapshot(traceId: string, snapshot: TraceSnapshot): void { function applyTraceSnapshot(traceId: string, snapshot: TraceSnapshot): void {
const trace = snapshotToRunnerTrace(snapshot); const trace = snapshotToRunnerTrace(snapshot);
messages.value = messages.value.map((message) => message.traceId === traceId ? { ...message, runnerTrace: mergeRunnerTrace(message.runnerTrace, trace), updatedAt: new Date().toISOString() } : message); messages.value = messages.value.map((message) => message.traceId === traceId ? { ...message, runnerTrace: mergeRunnerTrace(message.runnerTrace, trace), updatedAt: new Date().toISOString() } : message);
bumpConversationActivity({ traceId, sessionId: trace.sessionId, threadId: trace.threadId, status: firstNonEmptyString(trace.status), updatedAt: firstNonEmptyString(trace.updatedAt) });
} }
function completeTrace(traceId: string, result: AgentChatResultResponse): void { function completeTrace(traceId: string, result: AgentChatResultResponse): void {
@@ -295,6 +334,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message); const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message);
return { ...message, status: result.status === "completed" ? "completed" : statusFromResult(result.status), text, runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() }; return { ...message, status: result.status === "completed" ? "completed" : statusFromResult(result.status), text, runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() };
}); });
bumpConversationActivity({ conversationId: firstNonEmptyString((result as Record<string, unknown>).conversationId), sessionId: result.sessionId, threadId: result.threadId, traceId, status: statusFromResult(result.status), updatedAt: resultActivityUpdatedAt(result) });
chatPending.value = false; chatPending.value = false;
currentRequest.value = null; currentRequest.value = null;
void clearActiveTrace(traceId, "trace-terminal"); void clearActiveTrace(traceId, "trace-terminal");
@@ -319,6 +359,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message); const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message);
return { ...message, runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() }; return { ...message, runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() };
}); });
bumpConversationActivity({ conversationId: firstNonEmptyString((result as Record<string, unknown>).conversationId), sessionId: result.sessionId, threadId: result.threadId, traceId, status: statusFromResult(result.status), updatedAt: resultActivityUpdatedAt(result) });
} }
function failTrace(traceId: string, message: string): void { function failTrace(traceId: string, message: string): void {
@@ -330,6 +371,8 @@ export const useWorkbenchStore = defineStore("workbench", () => {
function markMessage(traceId: string, patch: Partial<ChatMessage>): void { function markMessage(traceId: string, patch: Partial<ChatMessage>): void {
messages.value = messages.value.map((message) => message.traceId === traceId && message.role === "agent" ? { ...message, ...patch, updatedAt: new Date().toISOString() } : message); messages.value = messages.value.map((message) => message.traceId === traceId && message.role === "agent" ? { ...message, ...patch, updatedAt: new Date().toISOString() } : message);
const updated = messages.value.find((message) => message.traceId === traceId && message.role === "agent");
bumpConversationActivity({ conversationId: updated?.conversationId, sessionId: updated?.sessionId, threadId: updated?.threadId, traceId, status: patch.status ?? updated?.status, updatedAt: updated?.updatedAt });
} }
async function ensureWorkspace(): Promise<WorkspaceRecord | null> { async function ensureWorkspace(): Promise<WorkspaceRecord | null> {
@@ -483,6 +526,12 @@ function recordValue(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" ? value as Record<string, unknown> : null; return value && typeof value === "object" ? value as Record<string, unknown> : null;
} }
function resultActivityUpdatedAt(result: AgentChatResultResponse): string | null {
const finalResponse = recordValue(result.finalResponse);
const reply = recordValue(result.reply);
return firstNonEmptyString(result.updatedAt, finalResponse?.updatedAt, reply?.updatedAt, finalResponse?.createdAt, reply?.createdAt);
}
function firstArray(...values: unknown[]): TraceEvent[] { function firstArray(...values: unknown[]): TraceEvent[] {
for (const value of values) { for (const value of values) {
if (Array.isArray(value)) return value as TraceEvent[]; if (Array.isArray(value)) return value as TraceEvent[];