fix: restore workbench messages from conversation detail
This commit is contained in:
@@ -5,16 +5,28 @@ export interface ConversationListOptions {
|
||||
includeConversationId?: string | null;
|
||||
}
|
||||
|
||||
export interface ConversationDetailOptions {
|
||||
projectId?: string | null;
|
||||
}
|
||||
|
||||
export const workbenchAPI = {
|
||||
workspace: (projectId: string): Promise<ApiResult<{ workspace?: WorkspaceRecord }>> => fetchJson(`/v1/workbench/workspace?projectId=${encodeURIComponent(projectId)}`, { timeoutMs: 8000, timeoutName: "workspace" }),
|
||||
updateWorkspace: (workspaceId: string, payload: Record<string, unknown>): Promise<ApiResult<{ workspace?: WorkspaceRecord }>> => fetchJson(`/v1/workbench/workspace/${encodeURIComponent(workspaceId)}`, { method: "PATCH", body: JSON.stringify(payload), timeoutMs: 8000, timeoutName: "workspace update" }),
|
||||
selectConversation: (workspaceId: string, payload: Record<string, unknown>): Promise<ApiResult<{ workspace?: WorkspaceRecord }>> => fetchJson(`/v1/workbench/workspace/${encodeURIComponent(workspaceId)}/select-conversation`, { method: "POST", body: JSON.stringify(payload), timeoutMs: 8000, timeoutName: "select conversation" }),
|
||||
conversations: (projectId: string, options: ConversationListOptions = {}): Promise<ApiResult<{ conversations?: ConversationRecord[] }>> => fetchJson(conversationListPath(projectId, options), { timeoutMs: 8000, timeoutName: "conversations" }),
|
||||
conversation: (conversationId: string): Promise<ApiResult<{ conversation?: ConversationRecord }>> => fetchJson(`/v1/agent/conversations/${encodeURIComponent(conversationId)}`, { timeoutMs: 8000, timeoutName: "conversation" }),
|
||||
conversation: (conversationId: string, options: ConversationDetailOptions = {}): Promise<ApiResult<{ conversation?: ConversationRecord }>> => fetchJson(conversationDetailPath(conversationId, options), { timeoutMs: 8000, timeoutName: "conversation" }),
|
||||
deleteConversation: (conversationId: string, payload: Record<string, unknown>): Promise<ApiResult<{ workspace?: WorkspaceRecord }>> => fetchJson(`/v1/agent/conversations/${encodeURIComponent(conversationId)}`, { method: "DELETE", body: JSON.stringify(payload), timeoutMs: 8000, timeoutName: "delete conversation" }),
|
||||
saveConversation: (conversationId: string, payload: Record<string, unknown>): Promise<ApiResult<{ ok?: boolean }>> => fetchJson(`/v1/agent/conversations/${encodeURIComponent(conversationId)}`, { method: "PUT", body: JSON.stringify(payload), timeoutMs: 8000, timeoutName: "save conversation" })
|
||||
};
|
||||
|
||||
function conversationDetailPath(conversationId: string, options: ConversationDetailOptions): string {
|
||||
const params = new URLSearchParams();
|
||||
const projectId = typeof options.projectId === "string" ? options.projectId.trim() : "";
|
||||
if (projectId) params.set("projectId", projectId);
|
||||
const query = params.toString();
|
||||
return `/v1/agent/conversations/${encodeURIComponent(conversationId)}${query ? `?${query}` : ""}`;
|
||||
}
|
||||
|
||||
function conversationListPath(projectId: string, options: ConversationListOptions): string {
|
||||
const params = new URLSearchParams({ projectId });
|
||||
const includeConversationId = typeof options.includeConversationId === "string" ? options.includeConversationId.trim() : "";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Router } from "vue-router";
|
||||
import { isNavigationFailure } from "vue-router";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { DEFAULT_WORKBENCH_PROJECT_ID, normalizeWorkbenchProjectId } from "@/utils";
|
||||
import { resolveDocumentTitle } from "./title";
|
||||
|
||||
const CHUNK_RELOAD_KEY = "hwlab:v03:chunk-reload-at";
|
||||
@@ -29,7 +30,7 @@ export function installChunkRecovery(router: Router): void {
|
||||
export function installRouterGuards(router: Router): void {
|
||||
router.beforeEach(async (to) => {
|
||||
const auth = useAuthStore();
|
||||
if (!auth.checked) await auth.bootstrap();
|
||||
if (!auth.checked) await auth.bootstrap(projectIdFromRoute(to));
|
||||
if (to.meta.requiresAuth !== false && auth.authState === "login") return { name: "Login", query: { redirect: to.fullPath } };
|
||||
if (to.meta.requiresAdmin === true && !auth.isAdmin) return { name: "Dashboard", query: { blocked: "admin_required" } };
|
||||
return true;
|
||||
@@ -42,3 +43,9 @@ export function installRouterGuards(router: Router): void {
|
||||
|
||||
installChunkRecovery(router);
|
||||
}
|
||||
|
||||
function projectIdFromRoute(to: { query?: Record<string, unknown> }): string {
|
||||
const raw = to.query?.projectId;
|
||||
const value = Array.isArray(raw) ? raw[0] : raw;
|
||||
return normalizeWorkbenchProjectId(value) ?? DEFAULT_WORKBENCH_PROJECT_ID;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
const projectId = ref(resolveInitialWorkbenchProjectId());
|
||||
const workspace = ref<WorkspaceRecord | null>(window.HWLAB_CLOUD_WEB_WORKSPACE_BOOTSTRAP?.workspace ?? null);
|
||||
const conversations = ref<ConversationRecord[]>([]);
|
||||
const messages = ref<ChatMessage[]>(messagesFromWorkspace(workspace.value));
|
||||
const messages = ref<ChatMessage[]>([]);
|
||||
const providerProfile = ref<ProviderProfile>(readString("hwlab.workbench.providerProfile.v1", "codex"));
|
||||
const providerOptions = ref<ProviderProfileOption[]>(defaultProviderProfileOptions(providerProfile.value));
|
||||
const recentDrafts = ref<DraftEntry[]>(readRecentDrafts());
|
||||
@@ -54,6 +54,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
|
||||
async function hydrate(): Promise<void> {
|
||||
const requestEpoch = workspaceSelectionEpoch.value;
|
||||
const previousConversationId = activeConversationId.value;
|
||||
conversationsReady.value = conversations.value.length > 0;
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
@@ -64,23 +65,36 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
return;
|
||||
}
|
||||
const nextWorkspace = workspaceResult.data?.workspace ?? null;
|
||||
const conversationsResult = await api.workbench.conversations(projectId.value, { includeConversationId: selectedConversationIdFromWorkspace(nextWorkspace) });
|
||||
const nextProjectId = workspaceProjectId(nextWorkspace, projectId.value);
|
||||
projectId.value = nextProjectId;
|
||||
const selectedConversationId = selectedConversationIdFromWorkspace(nextWorkspace);
|
||||
const [conversationsResult, selectedConversationResult] = await Promise.all([
|
||||
api.workbench.conversations(nextProjectId, { includeConversationId: selectedConversationId }),
|
||||
selectedConversationId ? api.workbench.conversation(selectedConversationId, { projectId: nextProjectId }) : Promise.resolve(null)
|
||||
]);
|
||||
loading.value = false;
|
||||
const selectedConversation = selectedConversationResult?.ok ? selectedConversationResult.data?.conversation ?? null : null;
|
||||
let nextConversations = conversations.value;
|
||||
if (conversationsResult.ok) {
|
||||
nextConversations = stableConversationList(conversations.value, conversationsResult.data?.conversations, selectedConversationIdFromWorkspace(nextWorkspace), selectedConversationFromWorkspace(nextWorkspace));
|
||||
nextConversations = stableConversationList(conversations.value, conversationsResult.data?.conversations, selectedConversationId, selectedConversation);
|
||||
conversations.value = nextConversations;
|
||||
conversationsReady.value = true;
|
||||
void refreshSessionStatusAuthority(nextConversations);
|
||||
} else {
|
||||
if (selectedConversation) {
|
||||
nextConversations = mergeConversationIntoList(nextConversations, selectedConversation);
|
||||
conversations.value = nextConversations;
|
||||
}
|
||||
conversationsReady.value = conversations.value.length > 0;
|
||||
error.value = conversations.value.length > 0 ? null : conversationsResult.error ?? "session list unavailable";
|
||||
}
|
||||
if (shouldApplyWorkspaceSnapshot({ requestEpoch, currentEpoch: workspaceSelectionEpoch.value, currentConversationId: activeConversationId.value, workspace: nextWorkspace })) {
|
||||
workspace.value = nextWorkspace;
|
||||
if (selectedConversationId && selectedConversationResult && !selectedConversationResult.ok) error.value = selectedConversationResult.error ?? "conversation unavailable";
|
||||
const workspaceSnapshot = selectedConversation ? workspaceWithSelectedConversation(nextWorkspace, selectedConversation, conversationProjectId(selectedConversation, nextProjectId)) : nextWorkspace;
|
||||
if (shouldApplyWorkspaceSnapshot({ requestEpoch, currentEpoch: workspaceSelectionEpoch.value, currentConversationId: activeConversationId.value, workspace: workspaceSnapshot })) {
|
||||
workspace.value = workspaceSnapshot;
|
||||
providerProfile.value = firstNonEmptyString(workspace.value?.providerProfile, workspace.value?.workspace?.providerProfile, providerProfile.value) ?? providerProfile.value;
|
||||
rememberWorkbenchProjectId(workspaceProjectId(workspace.value, projectId.value));
|
||||
messages.value = messagesFromWorkspaceSelection(workspace.value, nextConversations);
|
||||
rememberWorkbenchProjectId(workspaceProjectId(workspace.value, nextProjectId));
|
||||
messages.value = messagesFromSelectedConversation(selectedConversation, selectedConversationId, previousConversationId, messages.value);
|
||||
void hydrateTurnStatusAuthority(messages.value);
|
||||
void hydrateTerminalMessageDiagnostics();
|
||||
void hydrateTraceEvents(messages.value);
|
||||
@@ -128,24 +142,28 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
conversations.value = mergeConversationIntoList(conversations.value, conversation);
|
||||
conversationsReady.value = true;
|
||||
workspace.value = optimisticWorkspaceSelection(current, conversation, tabProjectId);
|
||||
messages.value = messagesFromConversation(conversation);
|
||||
void hydrateTurnStatusAuthority(messages.value);
|
||||
void hydrateTraceEvents(messages.value);
|
||||
void refreshSessionStatusById(sessionIdFromConversation(conversation));
|
||||
loading.value = true;
|
||||
const response = await api.workbench.selectConversation(current.workspaceId, { projectId: tabProjectId, conversationId: conversation.conversationId, sessionId: conversation.sessionId, threadId: conversation.threadId, updatedByClient: "cloud-web-vue" });
|
||||
const [response, detailResponse] = await Promise.all([
|
||||
api.workbench.selectConversation(current.workspaceId, { projectId: tabProjectId, conversationId: conversation.conversationId, sessionId: conversation.sessionId, threadId: conversation.threadId, updatedByClient: "cloud-web-vue" }),
|
||||
api.workbench.conversation(conversation.conversationId, { projectId: tabProjectId })
|
||||
]);
|
||||
loading.value = false;
|
||||
clearSwitchingConversation(conversation.conversationId);
|
||||
if (!isCurrentWorkspaceSelection(requestEpoch, conversation.conversationId)) return;
|
||||
if (response.ok) {
|
||||
workspace.value = response.data?.workspace ?? current;
|
||||
messages.value = messagesFromWorkspaceSelection(workspace.value, [conversation, ...conversations.value]);
|
||||
const selectedConversation = detailResponse.ok ? detailResponse.data?.conversation ?? null : null;
|
||||
if (!selectedConversation) error.value = detailResponse.error ?? "conversation unavailable";
|
||||
const selectedProjectId = conversationProjectId(selectedConversation ?? conversation, tabProjectId);
|
||||
workspace.value = workspaceWithSelectedConversation(response.data?.workspace ?? current, selectedConversation ?? conversation, selectedProjectId);
|
||||
messages.value = messagesFromSelectedConversation(selectedConversation, conversation.conversationId, null, []);
|
||||
if (selectedConversation) conversations.value = mergeConversationIntoList(conversations.value, selectedConversation);
|
||||
void hydrateTurnStatusAuthority(messages.value);
|
||||
void hydrateTerminalMessageDiagnostics();
|
||||
void hydrateTraceEvents(messages.value);
|
||||
reattachRestoredActiveTrace();
|
||||
currentRequest.value = null;
|
||||
await refreshSelectedSessionStatus(conversation);
|
||||
await refreshSelectedSessionStatus(selectedConversation ?? conversation);
|
||||
await refreshConversations(conversation.conversationId);
|
||||
return;
|
||||
}
|
||||
@@ -169,7 +187,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
await selectConversation(existing);
|
||||
return activeConversationId.value === normalized;
|
||||
}
|
||||
const response = await api.workbench.conversation(normalized);
|
||||
const response = await api.workbench.conversation(normalized, { projectId: activeProjectId.value });
|
||||
const conversation = response.data?.conversation ?? null;
|
||||
if (!response.ok || !conversation) {
|
||||
if (isCurrentWorkspaceSelection(requestEpoch, normalized)) clearSwitchingConversation(normalized);
|
||||
@@ -197,7 +215,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
async function refreshConversations(includeConversationId: string | null = currentListIncludeConversationId()): Promise<void> {
|
||||
const response = await api.workbench.conversations(activeProjectId.value, { includeConversationId });
|
||||
if (response.ok) {
|
||||
conversations.value = stableConversationList(conversations.value, response.data?.conversations, includeConversationId, activeConversation.value ?? selectedConversationFromWorkspace(workspace.value));
|
||||
conversations.value = stableConversationList(conversations.value, response.data?.conversations, includeConversationId, activeConversation.value);
|
||||
conversationsReady.value = true;
|
||||
await refreshSessionStatusAuthority(conversations.value);
|
||||
return;
|
||||
@@ -628,15 +646,20 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
const freshWorkspace = fresh.data?.workspace;
|
||||
if (!fresh.ok || !freshWorkspace?.workspaceId) return false;
|
||||
workspace.value = optimisticWorkspaceSelection(freshWorkspace, conversation, tabProjectId);
|
||||
messages.value = messagesFromConversation(conversation);
|
||||
const retried = await api.workbench.selectConversation(freshWorkspace.workspaceId, { projectId: tabProjectId, conversationId: conversation.conversationId, sessionId: conversation.sessionId, threadId: conversation.threadId, updatedByClient: "cloud-web-vue-retry" });
|
||||
const [retried, detailResponse] = await Promise.all([
|
||||
api.workbench.selectConversation(freshWorkspace.workspaceId, { projectId: tabProjectId, conversationId: conversation.conversationId, sessionId: conversation.sessionId, threadId: conversation.threadId, updatedByClient: "cloud-web-vue-retry" }),
|
||||
api.workbench.conversation(conversation.conversationId, { projectId: tabProjectId })
|
||||
]);
|
||||
if (!isCurrentWorkspaceSelection(requestEpoch, conversation.conversationId)) return true;
|
||||
if (!retried.ok) {
|
||||
error.value = retried.error ?? "session switch failed";
|
||||
return false;
|
||||
}
|
||||
workspace.value = retried.data?.workspace ?? workspace.value;
|
||||
messages.value = messagesFromWorkspaceSelection(workspace.value, [conversation, ...conversations.value]);
|
||||
const selectedConversation = detailResponse.ok ? detailResponse.data?.conversation ?? null : null;
|
||||
if (!selectedConversation) error.value = detailResponse.error ?? "conversation unavailable";
|
||||
workspace.value = workspaceWithSelectedConversation(retried.data?.workspace ?? workspace.value, selectedConversation ?? conversation, conversationProjectId(selectedConversation ?? conversation, tabProjectId));
|
||||
messages.value = messagesFromSelectedConversation(selectedConversation, conversation.conversationId, null, []);
|
||||
if (selectedConversation) conversations.value = mergeConversationIntoList(conversations.value, selectedConversation);
|
||||
void hydrateTurnStatusAuthority(messages.value);
|
||||
void hydrateTerminalMessageDiagnostics();
|
||||
void hydrateTraceEvents(messages.value);
|
||||
@@ -673,27 +696,57 @@ function uniqueTraceIds(messages: ChatMessage[]): string[] {
|
||||
}
|
||||
|
||||
function optimisticWorkspaceSelection(current: WorkspaceRecord, conversation: ConversationRecord, projectId: string): WorkspaceRecord {
|
||||
const sessionId = firstNonEmptyString(conversation.sessionId, conversation.session?.sessionId, current.selectedAgentSessionId, current.workspace?.selectedAgentSessionId);
|
||||
const threadId = firstNonEmptyString(conversation.threadId, conversation.session?.threadId, current.workspace?.threadId);
|
||||
return {
|
||||
...current,
|
||||
projectId,
|
||||
selectedConversationId: conversation.conversationId,
|
||||
selectedAgentSessionId: conversation.sessionId ?? current.selectedAgentSessionId,
|
||||
selectedAgentSessionId: sessionId,
|
||||
workspace: {
|
||||
...(current.workspace ?? {}),
|
||||
projectId,
|
||||
selectedConversationId: conversation.conversationId,
|
||||
selectedAgentSessionId: sessionId,
|
||||
threadId
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function workspaceWithSelectedConversation(current: WorkspaceRecord | null, conversation: ConversationRecord, projectId: string): WorkspaceRecord | null {
|
||||
if (!current) return current;
|
||||
const sessionId = firstNonEmptyString(conversation.sessionId, conversation.session?.sessionId, current.selectedAgentSessionId, current.workspace?.selectedAgentSessionId);
|
||||
const threadId = firstNonEmptyString(conversation.threadId, conversation.session?.threadId, current.workspace?.threadId);
|
||||
return {
|
||||
...current,
|
||||
projectId,
|
||||
selectedConversationId: conversation.conversationId,
|
||||
selectedAgentSessionId: sessionId,
|
||||
selectedConversation: conversation,
|
||||
workspace: {
|
||||
...(current.workspace ?? {}),
|
||||
projectId,
|
||||
selectedConversationId: conversation.conversationId,
|
||||
selectedAgentSessionId: conversation.sessionId ?? current.workspace?.selectedAgentSessionId,
|
||||
threadId: conversation.threadId ?? current.workspace?.threadId,
|
||||
messages: conversation.messages ?? []
|
||||
selectedAgentSessionId: sessionId,
|
||||
threadId
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function conversationProjectId(conversation: ConversationRecord | null | undefined, fallback: string): string {
|
||||
return firstNonEmptyString(conversation?.projectId, fallback) ?? DEFAULT_WORKBENCH_PROJECT_ID;
|
||||
}
|
||||
|
||||
function messagesFromConversation(conversation: ConversationRecord): ChatMessage[] {
|
||||
return (conversation.messages ?? []).map((message) => normalizeChatMessage(message));
|
||||
}
|
||||
|
||||
function messagesFromSelectedConversation(conversation: ConversationRecord | null, selectedConversationId: string | null, previousConversationId: string | null, previousMessages: ChatMessage[]): ChatMessage[] {
|
||||
if (conversation && (!selectedConversationId || conversation.conversationId === selectedConversationId)) return messagesFromConversation(conversation);
|
||||
if (selectedConversationId && previousConversationId === selectedConversationId && previousMessages.length > 0) return previousMessages;
|
||||
return [];
|
||||
}
|
||||
|
||||
function messagesFromWorkspace(workspace: WorkspaceRecord | null): ChatMessage[] {
|
||||
const selected = workspace?.selectedConversation?.messages;
|
||||
const embedded = workspace?.workspace?.messages;
|
||||
@@ -701,38 +754,6 @@ function messagesFromWorkspace(workspace: WorkspaceRecord | null): ChatMessage[]
|
||||
return source.map((message) => normalizeChatMessage(message));
|
||||
}
|
||||
|
||||
function messagesFromWorkspaceSelection(workspace: WorkspaceRecord | null, fallbackConversations: ConversationRecord[]): ChatMessage[] {
|
||||
const workspaceMessages = messagesFromWorkspace(workspace);
|
||||
if (workspaceMessages.length > 0) return workspaceMessages;
|
||||
const selectedConversationId = selectedConversationIdFromWorkspace(workspace);
|
||||
const fallback = selectedConversationId ? fallbackConversations.find((conversation) => conversation.conversationId === selectedConversationId) : null;
|
||||
return fallback ? messagesFromConversation(fallback) : [];
|
||||
}
|
||||
|
||||
function selectedConversationFromWorkspace(workspace: WorkspaceRecord | null): ConversationRecord | null {
|
||||
const selected = workspace?.selectedConversation;
|
||||
if (selected?.conversationId) {
|
||||
const messages = Array.isArray(selected.messages) && selected.messages.length > 0 ? selected.messages : messagesFromWorkspace(workspace);
|
||||
return { ...selected, messages, messageCount: selected.messageCount ?? (messages.length || undefined) };
|
||||
}
|
||||
const conversationId = selectedConversationIdFromWorkspace(workspace);
|
||||
if (!conversationId) return null;
|
||||
const messages = messagesFromWorkspace(workspace);
|
||||
const firstUserMessage = messages.find((message) => message.role === "user");
|
||||
return {
|
||||
conversationId,
|
||||
projectId: firstNonEmptyString(workspace?.projectId, workspace?.workspace?.projectId),
|
||||
sessionId: firstNonEmptyString(workspace?.selectedAgentSessionId, workspace?.workspace?.selectedAgentSessionId),
|
||||
threadId: firstNonEmptyString(workspace?.workspace?.threadId),
|
||||
status: firstNonEmptyString(workspace?.workspace?.sessionStatus),
|
||||
updatedAt: firstNonEmptyString(workspace?.updatedAt, workspace?.workspace?.updatedAt),
|
||||
lastTraceId: firstNonEmptyString(workspace?.activeTraceId, workspace?.workspace?.activeTraceId, workspace?.workspace?.lastTraceId),
|
||||
firstUserMessagePreview: firstNonEmptyString(firstUserMessage?.text, firstUserMessage?.content),
|
||||
messageCount: messages.length || undefined,
|
||||
messages
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeChatMessage(message: ChatMessage): ChatMessage {
|
||||
const runnerTrace = normalizeMessageRunnerTrace(message);
|
||||
const error = normalizeAgentError(message.error ?? runnerTrace?.error);
|
||||
|
||||
Reference in New Issue
Block a user