Merge pull request #1307 from pikasTech/fix/issue-1293-selected-session
fix(web): keep selected workbench session visible
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import type { ChatMessage, WorkspaceRecord } from "../src/types/index.ts";
|
||||
import { defaultProviderProfileOptions, normalizeRecentDrafts, providerProfileOptionsFromPayload, recordRecentDraft, resolveComposerState, shouldApplyWorkspaceSnapshot, sortSessionTabs, workspaceWithClearedActiveTrace } from "../src/stores/workbench-session.ts";
|
||||
import type { ChatMessage, ConversationRecord, WorkspaceRecord } from "../src/types/index.ts";
|
||||
import { defaultProviderProfileOptions, mergeSelectedConversation, normalizeRecentDrafts, providerProfileOptionsFromPayload, recordRecentDraft, resolveComposerState, shouldApplyWorkspaceSnapshot, sortSessionTabs, workspaceWithClearedActiveTrace } from "../src/stores/workbench-session.ts";
|
||||
|
||||
test("R2 composer ignores stale workspace activeTraceId without verified active message", () => {
|
||||
const workspace = workspaceRecord({ activeTraceId: "trc_stale", sessionStatus: "running" });
|
||||
@@ -77,6 +77,32 @@ test("R2 session tabs use latest message activity time instead of stale conversa
|
||||
assert.equal(tabs[0]?.updatedAt, "2026-01-04T00:01:00.000Z");
|
||||
});
|
||||
|
||||
test("R2 session tabs keep the workspace-selected conversation when the list window omits it", () => {
|
||||
const workspace: WorkspaceRecord = {
|
||||
...workspaceRecord({ activeTraceId: null, sessionStatus: "completed" }),
|
||||
selectedConversationId: "cnv_selected_omitted",
|
||||
selectedAgentSessionId: "ses_selected_omitted",
|
||||
selectedConversation: {
|
||||
conversationId: "cnv_selected_omitted",
|
||||
sessionId: "ses_selected_omitted",
|
||||
firstUserMessagePreview: "被列表窗口省略的当前会话",
|
||||
updatedAt: "2026-01-05T00:00:00.000Z",
|
||||
messages: [agentMessage({ conversationId: "cnv_selected_omitted", sessionId: "ses_selected_omitted", status: "completed", updatedAt: "2026-01-05T00:00:00.000Z" })]
|
||||
},
|
||||
workspace: {
|
||||
selectedConversationId: "cnv_selected_omitted",
|
||||
selectedAgentSessionId: "ses_selected_omitted",
|
||||
sessionStatus: "completed"
|
||||
}
|
||||
};
|
||||
const conversations: ConversationRecord[] = [{ conversationId: "cnv_other", sessionId: "ses_other", firstUserMessagePreview: "列表里较新的其他会话", updatedAt: "2026-01-06T00:00:00.000Z" }];
|
||||
const merged = mergeSelectedConversation(conversations, workspace);
|
||||
assert.equal(merged.some((conversation) => conversation.conversationId === "cnv_selected_omitted"), true);
|
||||
const tabs = sortSessionTabs(merged, "cnv_selected_omitted");
|
||||
assert.equal(tabs.find((tab) => tab.conversationId === "cnv_selected_omitted")?.active, true);
|
||||
assert.equal(tabs.find((tab) => tab.conversationId === "cnv_selected_omitted")?.label, "被列表窗口省略的当前会话");
|
||||
});
|
||||
|
||||
test("R2 session tab labels normalize object previews and reject object placeholders", () => {
|
||||
const tabs = sortSessionTabs([
|
||||
{ conversationId: "cnv_object", sessionId: "ses_object", firstUserMessagePreview: { text: "对象预览标题" } as unknown as string, updatedAt: "2026-01-03T00:00:00.000Z" },
|
||||
|
||||
@@ -76,6 +76,20 @@ export function shouldApplyWorkspaceSnapshot(input: { requestEpoch: number; curr
|
||||
return selectedConversationIdFromWorkspace(input.workspace) === currentConversationId;
|
||||
}
|
||||
|
||||
export function mergeSelectedConversation(conversations: ConversationRecord[], workspace: WorkspaceRecord | null): ConversationRecord[] {
|
||||
const selectedConversationId = selectedConversationIdFromWorkspace(workspace);
|
||||
if (!selectedConversationId) return conversations;
|
||||
const selected = selectedConversationFromWorkspace(workspace, selectedConversationId);
|
||||
if (!selected) return conversations;
|
||||
const existingIndex = conversations.findIndex((conversation) => conversation.conversationId === selectedConversationId);
|
||||
if (existingIndex < 0) return [selected, ...conversations];
|
||||
const existing = conversations[existingIndex];
|
||||
if (!existing) return [selected, ...conversations];
|
||||
const next = conversations.slice();
|
||||
next[existingIndex] = mergeConversationRecords(existing, selected);
|
||||
return next;
|
||||
}
|
||||
|
||||
export function workspaceWithClearedActiveTrace(workspace: WorkspaceRecord | null, traceId: string, reason: string): WorkspaceRecord | null {
|
||||
if (!workspace || !traceId) return workspace;
|
||||
const activeTraceId = activeTraceIdFromWorkspace(workspace);
|
||||
@@ -162,6 +176,51 @@ function activeConversation(conversations: ConversationRecord[], conversationId:
|
||||
return conversations.find((item) => item.conversationId === conversationId) ?? null;
|
||||
}
|
||||
|
||||
function selectedConversationFromWorkspace(workspace: WorkspaceRecord | null, selectedConversationId: string): ConversationRecord | null {
|
||||
const selected = workspace?.selectedConversation;
|
||||
const stub = selectedConversationStub(workspace, selectedConversationId);
|
||||
if (selected?.conversationId === selectedConversationId) return stub ? mergeConversationRecords(stub, selected) : selected;
|
||||
return stub;
|
||||
}
|
||||
|
||||
function selectedConversationStub(workspace: WorkspaceRecord | null, selectedConversationId: string): ConversationRecord | null {
|
||||
if (!workspace) return null;
|
||||
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;
|
||||
return {
|
||||
conversationId: selectedConversationId,
|
||||
projectId: firstNonEmptyString(workspace.projectId, workspace.workspace?.projectId) ?? null,
|
||||
sessionId,
|
||||
threadId,
|
||||
status: firstNonEmptyString(workspace.workspace?.sessionStatus, messages.at(-1)?.status) ?? null,
|
||||
updatedAt: latestTimestamp(workspace.updatedAt, workspace.workspace?.updatedAt, ...messages.flatMap((message) => [message.updatedAt, message.runnerTrace?.updatedAt, message.createdAt])),
|
||||
lastTraceId,
|
||||
messageCount: messages.length,
|
||||
messages
|
||||
};
|
||||
}
|
||||
|
||||
function mergeConversationRecords(existing: ConversationRecord, selected: ConversationRecord): ConversationRecord {
|
||||
const selectedMessages = selected.messages ?? [];
|
||||
const existingMessages = existing.messages ?? [];
|
||||
return {
|
||||
...existing,
|
||||
...selected,
|
||||
conversationId: existing.conversationId,
|
||||
projectId: firstNonEmptyString(selected.projectId, existing.projectId) ?? null,
|
||||
sessionId: firstNonEmptyString(selected.sessionId, selected.session?.sessionId, existing.sessionId, existing.session?.sessionId) ?? null,
|
||||
threadId: firstNonEmptyString(selected.threadId, selected.session?.threadId, existing.threadId, existing.session?.threadId) ?? null,
|
||||
status: firstNonEmptyString(selected.status, existing.status) ?? null,
|
||||
lastTraceId: firstNonEmptyString(selected.lastTraceId, existing.lastTraceId) ?? null,
|
||||
firstUserMessagePreview: firstNonEmptyString(selected.firstUserMessagePreview, existing.firstUserMessagePreview) ?? null,
|
||||
messageCount: selected.messageCount ?? (selectedMessages.length > 0 ? selectedMessages.length : existing.messageCount ?? existingMessages.length),
|
||||
messages: selectedMessages.length > 0 ? selectedMessages : existing.messages,
|
||||
updatedAt: latestTimestamp(selected.updatedAt, selected.snapshot?.updatedAt, existing.updatedAt, existing.snapshot?.updatedAt)
|
||||
};
|
||||
}
|
||||
|
||||
function findActiveAgentMessage(messages: ChatMessage[], activeConversationId: string | null): ChatMessage | null {
|
||||
for (const message of [...messages].reverse()) {
|
||||
if (message.role !== "agent") continue;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { api } from "@/api";
|
||||
import { mergeRunnerTrace, snapshotToRunnerTrace, subscribeToTrace, type TraceSnapshot } from "@/composables/useTraceSubscription";
|
||||
import type { AgentChatResponse, AgentChatResultResponse, AgentRunProvenance, ApiResult, ChatMessage, ConversationRecord, LiveSurface, ProviderProfile, TraceEvent, WorkspaceRecord } from "@/types";
|
||||
import { DEFAULT_WORKBENCH_PROJECT_ID, firstNonEmptyString, nextProtocolId, rememberWorkbenchProjectId, resolveInitialWorkbenchProjectId, workspaceProjectId } from "@/utils";
|
||||
import { RECENT_DRAFTS_STORAGE_KEY, activeTraceIdFromWorkspace, defaultProviderProfileOptions, normalizeRecentDrafts, providerProfileOptionsFromPayload, recordRecentDraft, resolveComposerState, shouldApplyWorkspaceSnapshot, sortSessionTabs, workspaceWithClearedActiveTrace, type DraftEntry, type ProviderProfileOption } from "./workbench-session";
|
||||
import { RECENT_DRAFTS_STORAGE_KEY, activeTraceIdFromWorkspace, defaultProviderProfileOptions, mergeSelectedConversation, normalizeRecentDrafts, providerProfileOptionsFromPayload, recordRecentDraft, resolveComposerState, shouldApplyWorkspaceSnapshot, sortSessionTabs, workspaceWithClearedActiveTrace, type DraftEntry, type ProviderProfileOption } from "./workbench-session";
|
||||
|
||||
const DEFAULT_CODE_AGENT_TIMEOUT_MS = 1_800_000;
|
||||
const DEFAULT_GATEWAY_TIMEOUT_MS = 120_000;
|
||||
@@ -29,13 +29,14 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
const currentRequest = ref<{ traceId: string; conversationId: string | null; sessionId: string | null; threadId: string | null; status?: string | null } | null>(null);
|
||||
const workspaceSelectionEpoch = ref(0);
|
||||
|
||||
const visibleConversations = computed(() => mergeSelectedConversation(conversations.value, workspace.value));
|
||||
const activeConversationId = computed(() => firstNonEmptyString(switchingConversationId.value, workspace.value?.selectedConversationId, workspace.value?.workspace?.selectedConversationId, messages.value.find((message) => message.conversationId)?.conversationId));
|
||||
const selectedSessionId = computed(() => firstNonEmptyString(workspace.value?.selectedAgentSessionId, workspace.value?.workspace?.selectedAgentSessionId, activeConversation.value?.sessionId));
|
||||
const selectedThreadId = computed(() => firstNonEmptyString(workspace.value?.workspace?.threadId, activeConversation.value?.threadId));
|
||||
const activeConversation = computed(() => conversations.value.find((item) => item.conversationId === activeConversationId.value) ?? null);
|
||||
const sessionTabs = computed(() => sortSessionTabs(conversations.value, activeConversationId.value));
|
||||
const activeConversation = computed(() => visibleConversations.value.find((item) => item.conversationId === activeConversationId.value) ?? null);
|
||||
const sessionTabs = computed(() => sortSessionTabs(visibleConversations.value, activeConversationId.value));
|
||||
const activeProjectId = computed(() => workspaceProjectId(workspace.value, projectId.value));
|
||||
const composer = computed(() => resolveComposerState({ workspace: workspace.value, messages: messages.value, conversations: conversations.value, activeConversationId: activeConversationId.value, chatPending: chatPending.value, currentRequest: currentRequest.value }));
|
||||
const composer = computed(() => resolveComposerState({ workspace: workspace.value, messages: messages.value, conversations: visibleConversations.value, activeConversationId: activeConversationId.value, chatPending: chatPending.value, currentRequest: currentRequest.value }));
|
||||
|
||||
function recordActivity(label = "user-activity"): void {
|
||||
const now = Date.now();
|
||||
|
||||
Reference in New Issue
Block a user