Merge pull request #1325 from pikasTech/fix/issue-1317-navigation-bounce

fix: 防止 Workbench 异步状态把导航拉回
This commit is contained in:
Lyon
2026-06-16 10:51:42 +08:00
committed by GitHub
3 changed files with 65 additions and 4 deletions
@@ -0,0 +1,22 @@
import assert from "node:assert/strict";
import test from "node:test";
import { isWorkbenchNavigationContext, shouldReflectWorkbenchConversationUrl } from "../src/router/workbench-navigation.ts";
test("workbench navigation context recognizes workbench routes only while the component is active", () => {
assert.equal(isWorkbenchNavigationContext({ componentActive: true, routeSection: "workbench", routeName: "CodeWorkbenchSession", routePath: "/workbench/sessions/cnv_1" }), true);
assert.equal(isWorkbenchNavigationContext({ componentActive: true, routeName: "CodeWorkbench", routePath: "/workspace" }), true);
assert.equal(isWorkbenchNavigationContext({ componentActive: true, routeSection: "user", routeName: "Dashboard", routePath: "/dashboard" }), false);
assert.equal(isWorkbenchNavigationContext({ componentActive: false, routeSection: "workbench", routeName: "CodeWorkbenchSession", routePath: "/workbench/sessions/cnv_1" }), false);
});
test("workbench URL reflection never overrides a user navigation outside Workbench", () => {
assert.equal(shouldReflectWorkbenchConversationUrl({ componentActive: true, routeSection: "user", routeName: "Dashboard", routePath: "/dashboard", routeConversationId: null, conversationId: "cnv_async" }), false);
assert.equal(shouldReflectWorkbenchConversationUrl({ componentActive: false, routeSection: "workbench", routeName: "CodeWorkbench", routePath: "/workbench", routeConversationId: null, conversationId: "cnv_async" }), false);
});
test("workbench URL reflection still updates empty Workbench routes and respects route hydration", () => {
assert.equal(shouldReflectWorkbenchConversationUrl({ componentActive: true, routeSection: "workbench", routeName: "CodeWorkbench", routePath: "/workbench", routeConversationId: null, conversationId: "cnv_selected" }), true);
assert.equal(shouldReflectWorkbenchConversationUrl({ componentActive: true, routeSection: "workbench", routeName: "CodeWorkbenchSession", routePath: "/workbench/sessions/cnv_selected", routeConversationId: "cnv_selected", conversationId: "cnv_selected" }), false);
assert.equal(shouldReflectWorkbenchConversationUrl({ componentActive: true, routeSection: "workbench", routeName: "CodeWorkbenchSession", routePath: "/workbench/sessions/cnv_route", routeConversationId: "cnv_route", conversationId: "cnv_store", applyingRouteConversation: true }), false);
});
@@ -0,0 +1,27 @@
export interface WorkbenchNavigationContext {
componentActive: boolean;
routeName?: unknown;
routePath?: unknown;
routeSection?: unknown;
routeConversationId?: string | null;
conversationId?: string | null;
applyingRouteConversation?: boolean;
}
const WORKBENCH_ROUTE_NAMES = new Set(["CodeWorkbench", "CodeWorkbenchSession"]);
export function isWorkbenchNavigationContext(input: WorkbenchNavigationContext): boolean {
if (!input.componentActive) return false;
if (input.routeSection === "workbench") return true;
if (typeof input.routeName === "string" && WORKBENCH_ROUTE_NAMES.has(input.routeName)) return true;
if (typeof input.routePath !== "string") return false;
return input.routePath === "/workbench" || input.routePath === "/workspace" || input.routePath.startsWith("/workbench/") || input.routePath.startsWith("/workspace/");
}
export function shouldReflectWorkbenchConversationUrl(input: WorkbenchNavigationContext): boolean {
if (!input.conversationId) return false;
if (!isWorkbenchNavigationContext(input)) return false;
if (input.routeConversationId === input.conversationId) return false;
if (input.applyingRouteConversation && input.routeConversationId && input.routeConversationId !== input.conversationId) return false;
return true;
}
@@ -1,11 +1,12 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from "vue";
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import CommandComposer from "@/components/workbench/CommandComposer.vue";
import ConversationPanel from "@/components/workbench/ConversationPanel.vue";
import SessionRail from "@/components/workbench/SessionRail.vue";
import HwpodNodeOpsPanel from "@/components/hwpod/HwpodNodeOpsPanel.vue";
import { useAutoRefresh } from "@/composables/useAutoRefresh";
import { shouldReflectWorkbenchConversationUrl } from "@/router/workbench-navigation";
import { useWorkbenchStore } from "@/stores/workbench";
import { DEFAULT_WORKBENCH_PROJECT_ID, normalizeWorkbenchConversationId, normalizeWorkbenchProjectId } from "@/utils";
@@ -14,8 +15,10 @@ const route = useRoute();
const router = useRouter();
const routeConversationId = computed(() => normalizeWorkbenchConversationId(route.params.conversationId));
const applyingRouteConversation = ref(Boolean(routeConversationId.value));
const componentActive = ref(false);
onMounted(async () => {
componentActive.value = true;
applyingRouteConversation.value = Boolean(routeConversationId.value);
try {
await workbench.hydrate();
@@ -25,6 +28,9 @@ onMounted(async () => {
}
if (!routeConversationId.value || routeConversationId.value === workbench.activeConversationId) await reflectActiveConversationInUrl(workbench.activeConversationId);
});
onBeforeUnmount(() => {
componentActive.value = false;
});
watch(routeConversationId, () => void applyRouteConversation());
watch(() => workbench.activeConversationId, (conversationId) => void reflectActiveConversationInUrl(conversationId));
useAutoRefresh(() => workbench.refreshLive(), 30_000);
@@ -42,10 +48,16 @@ async function applyRouteConversation(): Promise<boolean> {
async function reflectActiveConversationInUrl(value: string | null): Promise<void> {
const conversationId = normalizeWorkbenchConversationId(value);
if (!conversationId) return;
const routeTarget = routeConversationId.value;
if (applyingRouteConversation.value && routeTarget && routeTarget !== conversationId) return;
if (routeTarget === conversationId) return;
if (!shouldReflectWorkbenchConversationUrl({
componentActive: componentActive.value,
routeName: route.name,
routePath: route.path,
routeSection: route.meta.section,
routeConversationId: routeTarget,
conversationId,
applyingRouteConversation: applyingRouteConversation.value
})) return;
const projectId = normalizeWorkbenchProjectId(route.query.projectId) ?? normalizeWorkbenchProjectId(workbench.workspace?.projectId) ?? normalizeWorkbenchProjectId(workbench.workspace?.workspace?.projectId);
await router.replace({
name: "CodeWorkbenchSession",