fix: guard workbench URL reflection
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user