Merge pull request #1330 from pikasTech/fix/1329-refresh-restore

fix(web): restore running Code Agent session after refresh
This commit is contained in:
Lyon
2026-06-16 14:45:43 +08:00
committed by GitHub
4 changed files with 63 additions and 16 deletions
+18 -6
View File
@@ -2742,25 +2742,37 @@ function resolvedConversationStatus(status, snapshot, messages = []) {
const snapshotStatus = textOr(snapshot.sessionStatus ?? snapshot.status, "").toLowerCase();
const latestAgent = latestAgentConversationMessage(messages);
const latestStatus = textOr(latestAgent?.status, "").toLowerCase();
if (["running", "busy", "pending", "active"].includes(stored)) {
if (snapshotStatus && !["running", "busy", "pending", "active"].includes(snapshotStatus)) return snapshotStatus === "cancelled" ? "canceled" : snapshotStatus;
if (latestStatus && !["running", "busy", "pending", "active"].includes(latestStatus)) return latestStatus === "cancelled" ? "canceled" : latestStatus;
if (isActiveConversationStatus(stored)) {
if (snapshotStatus && !isActiveConversationStatus(snapshotStatus)) return snapshotStatus === "cancelled" ? "canceled" : snapshotStatus;
if (latestStatus && !isActiveConversationStatus(latestStatus)) return latestStatus === "cancelled" ? "canceled" : latestStatus;
}
if (isActiveConversationStatus(latestStatus) && !isTerminalConversationStatus(stored) && !isTerminalConversationStatus(snapshotStatus)) return latestStatus === "active" ? "running" : latestStatus;
return stored || snapshotStatus || latestStatus || "idle";
}
function resolvedConversationLastTraceId(storedTraceId, status, messages = [], snapshot = {}) {
const latestTraceId = textOr(latestAgentConversationMessage(messages)?.traceId, "");
const latestAgent = latestAgentConversationMessage(messages);
const latestTraceId = textOr(latestAgent?.traceId, "");
const latestStatus = textOr(latestAgent?.status, "").toLowerCase();
const snapshotTraceId = textOr(snapshot.lastTraceId, "");
const stored = textOr(storedTraceId, "");
const snapshotStatus = textOr(snapshot.sessionStatus ?? snapshot.status, "").toLowerCase();
if (stored && snapshotStatus && !["running", "busy", "pending", "active"].includes(snapshotStatus)) return stored;
if (!["running", "busy", "pending", "active"].includes(textOr(status, "").toLowerCase())) {
if (latestTraceId && (isActiveConversationStatus(latestStatus) || isActiveConversationStatus(status))) return latestTraceId;
if (stored && snapshotStatus && !isActiveConversationStatus(snapshotStatus)) return stored;
if (!isActiveConversationStatus(status)) {
return latestTraceId || snapshotTraceId || stored;
}
return stored || latestTraceId || snapshotTraceId;
}
function isActiveConversationStatus(value) {
return ["running", "busy", "pending", "active", "processing", "accepted", "creating"].includes(textOr(value, "").toLowerCase().replace(/_/gu, "-"));
}
function isTerminalConversationStatus(value) {
return ["completed", "failed", "blocked", "timeout", "canceled", "cancelled", "stale", "thread-resume-failed", "interrupted", "expired"].includes(textOr(value, "").toLowerCase().replace(/_/gu, "-"));
}
function latestAgentConversationMessage(messages = []) {
if (!Array.isArray(messages)) return null;
for (const message of [...messages].reverse()) {
@@ -169,7 +169,7 @@ test("R2 active in-flight turn updates the selected session tab before list refr
assert.equal(tabs[0]?.messageCount, 2);
});
test("R2 session tabs use conversation status before stale snapshot or message status", () => {
test("R2 session tabs keep restored running message ahead of stale idle conversation status", () => {
const conversation: ConversationRecord = {
conversationId: "cnv_stale_snapshot",
sessionId: "ses_stale_snapshot",
@@ -177,9 +177,10 @@ test("R2 session tabs use conversation status before stale snapshot or message s
snapshot: { sessionStatus: "running" },
messages: [agentMessage({ conversationId: "cnv_stale_snapshot", status: "running", traceId: "trc_stale" })]
};
assert.equal(resolveConversationSessionStatus(conversation), "idle");
assert.equal(resolveConversationSessionStatus(conversation), "running");
const tabs = sortSessionTabs([conversation], "cnv_stale_snapshot");
assert.equal(tabs[0]?.running, false);
assert.equal(tabs[0]?.running, true);
assert.equal(tabs[0]?.lastTraceId, "trc_stale");
});
test("R2 agent message titles drop transient running wording", () => {
@@ -135,7 +135,7 @@ export function conversationToSessionTab(conversation: ConversationRecord, activ
const conversationId = conversation.conversationId;
const updatedAt = conversationDisplayUpdatedAt(conversation);
const status = resolveConversationSessionStatus(conversation);
const trace = firstNonEmptyString(conversation.lastTraceId, latestAgentMessage(conversation.messages)?.traceId, conversation.messages?.at(-1)?.traceId);
const trace = firstNonEmptyString(latestAgentMessage(conversation.messages)?.traceId, conversation.messages?.at(-1)?.traceId, conversation.lastTraceId);
const userMessage = conversation.messages?.find((message) => message.role === "user");
const running = isActiveStatus(status);
const preview = firstReadableSentence(
@@ -179,6 +179,17 @@ export function resolveConversationSessionStatus(conversation: ConversationRecor
latestMessage?.status
);
if (stored && isActiveStatus(stored) && terminalEvidence) return terminalEvidence;
const activeEvidence = firstActiveStatus(
latestAgent?.status,
latestAgent?.runnerTrace?.status,
latestAgent?.runnerTrace?.traceStatus,
latestMessage?.status,
...extraStatuses,
conversation.session?.status,
conversation.snapshot?.sessionStatus,
conversation.snapshot?.status
);
if (activeEvidence && !isTerminalStatus(stored)) return activeEvidence;
return stored ?? firstNormalizedSessionStatus(
...extraStatuses,
conversation.session?.status,
@@ -285,7 +296,7 @@ function selectedConversationStub(workspace: WorkspaceRecord | null, selectedCon
const messages = Array.isArray(workspace.workspace?.messages) ? workspace.workspace.messages : [];
const sessionId = firstNonEmptyString(workspace.selectedAgentSessionId, workspace.workspace?.selectedAgentSessionId, messages.find((message) => message.sessionId)?.sessionId) ?? null;
const threadId = firstNonEmptyString(workspace.workspace?.threadId, messages.find((message) => message.threadId)?.threadId) ?? null;
const lastTraceId = firstNonEmptyString(workspace.workspace?.lastTraceId, workspace.activeTraceId, workspace.workspace?.activeTraceId, [...messages].reverse().find((message) => message.traceId)?.traceId) ?? null;
const lastTraceId = firstNonEmptyString([...messages].reverse().find((message) => message.traceId)?.traceId, workspace.workspace?.lastTraceId, workspace.activeTraceId, workspace.workspace?.activeTraceId) ?? null;
const lastUserMessageAt = latestUserMessageAtFromMessages(messages);
const latestAgent = latestAgentMessage(messages);
const stub: ConversationRecord = {
@@ -325,7 +336,7 @@ function mergeConversationRecords(existing: ConversationRecord, selected: Conver
sessionId: firstNonEmptyString(primary.sessionId, primary.session?.sessionId, supplemental.sessionId, supplemental.session?.sessionId) ?? null,
threadId: firstNonEmptyString(primary.threadId, primary.session?.threadId, supplemental.threadId, supplemental.session?.threadId) ?? null,
status,
lastTraceId: firstNonEmptyString(primary.lastTraceId, supplemental.lastTraceId, latestAgentMessage(messages)?.traceId, messages.at(-1)?.traceId) ?? null,
lastTraceId: firstNonEmptyString(latestAgentMessage(messages)?.traceId, messages.at(-1)?.traceId, primary.lastTraceId, supplemental.lastTraceId) ?? null,
firstUserMessagePreview: firstNonEmptyString(primary.firstUserMessagePreview, primary.snapshot?.firstUserMessagePreview, supplemental.firstUserMessagePreview, supplemental.snapshot?.firstUserMessagePreview) ?? null,
messageCount: messages.length > 0 ? messages.length : primary.messageCount ?? supplemental.messageCount ?? primaryMessages.length,
messages: messages.length > 0 ? messages : primary.messages,
@@ -378,6 +389,14 @@ function firstNonActiveStatus(...values: unknown[]): string | null {
return null;
}
function firstActiveStatus(...values: unknown[]): string | null {
for (const value of values) {
const status = normalizeSessionStatus(value);
if (status && isActiveStatus(status)) return status;
}
return null;
}
function firstNormalizedSessionStatus(...values: unknown[]): string | null {
for (const value of values) {
const status = normalizeSessionStatus(value);
+19 -4
View File
@@ -66,8 +66,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
rememberWorkbenchProjectId(workspaceProjectId(workspace.value, projectId.value));
messages.value = messagesFromWorkspaceSelection(workspace.value, nextConversations);
void hydrateTerminalMessageDiagnostics();
const activeTraceId = activeTraceIdFromWorkspace(workspace.value);
if (activeTraceId) void validateAndReattachTrace(activeTraceId);
reattachRestoredActiveTrace();
}
await refreshProviderOptions();
}
@@ -117,6 +116,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
workspace.value = response.data?.workspace ?? current;
messages.value = messagesFromWorkspaceSelection(workspace.value, [conversation, ...conversations.value]);
void hydrateTerminalMessageDiagnostics();
reattachRestoredActiveTrace();
currentRequest.value = null;
return;
}
@@ -477,10 +477,16 @@ export const useWorkbenchStore = defineStore("workbench", () => {
workspace.value = retried.data?.workspace ?? workspace.value;
messages.value = messagesFromWorkspaceSelection(workspace.value, [conversation, ...conversations.value]);
void hydrateTerminalMessageDiagnostics();
reattachRestoredActiveTrace();
currentRequest.value = null;
return true;
}
function reattachRestoredActiveTrace(): void {
const traceId = firstNonEmptyString(activeTraceIdFromMessages(messages.value), activeTraceIdFromWorkspace(workspace.value));
if (traceId) void validateAndReattachTrace(traceId);
}
return { projectId, workspace, conversations, messages, providerProfile, providerOptions, recentDrafts, codeAgentTimeoutMs, gatewayShellTimeoutMs, live, loading, sessionListLoading, chatPending, error, activeConversationId, selectedSessionId, selectedThreadId, sessionTabs, composer, hydrate, refreshLive, createSession, selectConversation, selectConversationById, deleteCurrentSession, refreshConversations, submitMessage, cancelAgentMessage, cancelRunningTrace, retryAgentMessage, replayAgentTrace, setProviderProfile, refreshProviderOptions, pickDraft, clearRecentDrafts, clearConversation, recordActivity };
});
@@ -537,13 +543,12 @@ function normalizeChatMessageStatus(value: unknown, runnerTrace: ChatMessage["ru
const trace = normalizedStatusText(firstNonEmptyString(runnerTrace?.status, runnerTrace?.traceStatus));
const sessionTerminal = terminalChatStatus(session);
if (sessionTerminal && !terminalChatStatus(explicit)) return sessionTerminal;
if (session && !isTraceActiveStatus(session) && isTraceActiveStatus(explicit)) return "source";
const traceTerminal = terminalChatStatus(trace);
if (traceTerminal && (!explicit || explicit === "idle" || explicit === "source" || isTraceActiveStatus(explicit))) return traceTerminal;
const explicitTerminal = terminalChatStatus(explicit);
if (explicitTerminal) return explicitTerminal;
if (explicit === "sent") return "sent";
if (isTraceActiveStatus(explicit) || (!session || isTraceActiveStatus(session)) && isTraceActiveStatus(trace)) return "running";
if (isTraceActiveStatus(explicit) || isTraceActiveStatus(trace)) return "running";
if (explicit === "blocked") return "blocked";
if (explicit === "timeout") return "timeout";
if (explicit === "failed" || explicit === "error") return "failed";
@@ -560,6 +565,16 @@ function terminalChatStatus(value: string | null): ChatMessage["status"] | null
return null;
}
function activeTraceIdFromMessages(messages: ChatMessage[]): string | null {
for (const message of [...messages].reverse()) {
if (message.role !== "agent") continue;
if (!isTraceActiveStatus(message.status) && !isTraceActiveStatus(message.runnerTrace?.status) && !isTraceActiveStatus(message.runnerTrace?.traceStatus)) continue;
const traceId = firstNonEmptyString(message.traceId, message.runnerTrace?.traceId);
if (traceId) return traceId;
}
return null;
}
function normalizedStatusText(value: unknown): string | null {
const text = firstNonEmptyString(value);
return text ? text.trim().toLowerCase().replace(/_/gu, "-") : null;