mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix(ui): keep the workspace in the address on a project switch (A5)
The project switcher turned "?project=django-demo&workspace=galaxy" into "?project=cbm", so a reload could open another workspace. The new address now keeps the page-wide parameters App names (the workspace and the walk bounds) and nothing of the old project, and the address always names the workspace in view, also one carried over a switch or chosen from storage. Browser Back and Forward between projects work as before. Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
This commit is contained in:
1 parent
edf241786f
commit
7c02114709
3 files changed
+65
-9
No files matched your search
@@ -311,7 +311,7 @@ import type { SettingsMeasurement } from './settings/SettingsPanel';
|
||||
import AddProjectIndexDialog from './projects/AddProjectIndexDialog';
|
||||
import ProjectSwitcher from './projects/ProjectSwitcher';
|
||||
import type { ProjectsSource } from './projects/ProjectsPanel';
|
||||
import { ProjectWindows, type ProjectWindow } from './app/project-windows';
|
||||
import { ProjectWindows, useParamInAddress, type ProjectWindow } from './app/project-windows';
|
||||
import { MODEL_SUGGESTIONS, fetchCommand } from './settings/model-catalog';
|
||||
import { modelKey, readModelPreference, recordModelPreference } from './settings/model-preference';
|
||||
import {
|
||||
@@ -758,10 +758,12 @@ interface WindowCarry {
|
||||
/**
|
||||
* Jedes Projekt bekommt ein frisches Fenster in derselben Seite (K24, Kopf von
|
||||
* src/app/project-windows.tsx). Der Agent reicht beim Wechsel sein geladenes
|
||||
* Modell an das Fenster des neuen Projekts weiter.
|
||||
* Modell an das Fenster des neuen Projekts weiter. Die Adresse behaelt dabei
|
||||
* den Arbeitsbereich und die Grenzen des Walks, nichts vom alten Projekt (A5).
|
||||
*/
|
||||
const PAGE_PARAMS = ['workspace', CLOSURE_DEPTH_PARAM, CLOSURE_CAP_PARAM] as const;
|
||||
export default function App(): JSX.Element {
|
||||
return <ProjectWindows<WindowCarry> around={switchKeepingAgent}>
|
||||
return <ProjectWindows<WindowCarry> around={switchKeepingAgent} keepParams={PAGE_PARAMS}>
|
||||
{shown => <ProjectWindowView key={shown.key} projectWindow={shown} />}
|
||||
</ProjectWindows>;
|
||||
}
|
||||
@@ -783,11 +785,10 @@ function ProjectWindowView({ projectWindow }: { projectWindow: ProjectWindow<Win
|
||||
setProactiveSelection(undefined); setChatGraphSelection(undefined); setChatAttachment(undefined);
|
||||
}
|
||||
setWorkspace(value);
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('workspace', value);
|
||||
window.history.replaceState(window.history.state, '', url);
|
||||
try { localStorage.setItem('cbm.workspace', value); } catch { /* Session-only preference. */ }
|
||||
};
|
||||
// The address always names the workspace in view, also one carried over a project switch, so a reload keeps it (A5).
|
||||
useParamInAddress('workspace', workspace);
|
||||
const [welcomeOpen, setWelcomeOpen] = useState(() => {
|
||||
try { return localStorage.getItem('cbm.workspace.setup') !== 'done'; } catch { return false; }
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { act, useEffect } from 'react';
|
||||
import { createRoot, type Root } from 'react-dom/client';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ProjectWindows, type ProjectWindow } from './project-windows';
|
||||
import { ProjectWindows, useParamInAddress, type ProjectWindow } from './project-windows';
|
||||
|
||||
/*
|
||||
* K24: a project switch stays in the page. Each project gets a fresh window (fresh state in
|
||||
@@ -77,6 +77,34 @@ describe('an in-page project switch (K24)', () => {
|
||||
expect(mounted).toHaveLength(3);
|
||||
});
|
||||
|
||||
/* Hand test 2026-10-04 (A5): the switcher turned "?project=django-demo&workspace=galaxy" into "?project=cbm". */
|
||||
it('keeps the page-wide parameters it is given, the workspace among them, and nothing project specific', async () => {
|
||||
window.history.replaceState(null, '', '/?project=django-demo&workspace=galaxy&codeatlasClosureDepth=4&file=django%2Fshortcuts.py');
|
||||
await act(async () => root.render(<ProjectWindows<Carry> keepParams={['workspace', 'codeatlasClosureDepth', 'codeatlasClosureCap']}>
|
||||
{shown => <Window key={shown.key} window={shown} />}</ProjectWindows>));
|
||||
await act(async () => current!.open('atlas sample'));
|
||||
expect(window.location.search).toBe('?project=atlas%20sample&workspace=galaxy&codeatlasClosureDepth=4');
|
||||
expect(shownProject()).toBe('atlas sample');
|
||||
// Back and Forward of the browser still switch between both projects, each with its own address.
|
||||
const popped = () => new Promise<void>(resolve => window.addEventListener('popstate', () => resolve(), { once: true }));
|
||||
await act(async () => { const done = popped(); window.history.back(); await done; });
|
||||
expect(window.location.search).toBe('?project=django-demo&workspace=galaxy&codeatlasClosureDepth=4&file=django%2Fshortcuts.py');
|
||||
expect(shownProject()).toBe('django-demo');
|
||||
await act(async () => { const done = popped(); window.history.forward(); await done; });
|
||||
expect(shownProject()).toBe('atlas sample');
|
||||
});
|
||||
|
||||
it('writes the workspace in view into the address without a new history entry', async () => {
|
||||
window.history.replaceState(null, '', '/?project=django-demo');
|
||||
const length = window.history.length;
|
||||
function Workspace({ name }: { name: string }) { useParamInAddress('workspace', name); return null; }
|
||||
await act(async () => root.render(<Workspace name="architecture" />));
|
||||
expect(window.location.search).toBe('?project=django-demo&workspace=architecture');
|
||||
await act(async () => root.render(<Workspace name="galaxy" />));
|
||||
expect(window.location.search).toBe('?project=django-demo&workspace=galaxy');
|
||||
expect(window.history.length).toBe(length);
|
||||
});
|
||||
|
||||
it('commits the switch inside the frame it is given, so the old window is gone and the new one there when it ends', async () => {
|
||||
const seen: string[] = [];
|
||||
const around = (commit: () => void) => { seen.push(`before:${mounted.join(',')}`); commit(); seen.push(`after:${mounted.join(',')}|${unmounted.join(',')}`); };
|
||||
|
||||
@@ -33,6 +33,30 @@ export function projectOfLocation(search = window.location.search): string {
|
||||
return new URLSearchParams(search).get('project') ?? '';
|
||||
}
|
||||
|
||||
/*
|
||||
* Handtest vom 2026-10-04 (A5): der Wechsel machte aus
|
||||
* "?project=django-demo&workspace=galaxy" die Adresse "?project=cbm", und ein
|
||||
* Neuladen danach konnte einen anderen Arbeitsbereich zeigen. Die Adresse des
|
||||
* neuen Projekts behaelt nun die Parameter der ganzen Seite, die der Aufrufer
|
||||
* nennt (den Arbeitsbereich, die Grenzen des Walks), und nichts, was zum alten
|
||||
* Projekt gehoert.
|
||||
*/
|
||||
export function projectSwitchHref(project: string, search: string, keep: readonly string[]): string {
|
||||
const from = new URLSearchParams(search);
|
||||
const kept = keep.flatMap(name => { const value = from.get(name); return value === null ? [] : [`&${encodeURIComponent(name)}=${encodeURIComponent(value)}`]; });
|
||||
return `${projectHref(project)}${kept.join('')}`;
|
||||
}
|
||||
|
||||
/** Haelt einen Parameter der Adresse auf dem Wert, den die Seite zeigt, ohne neuen Eintrag im Verlauf (A5: der Arbeitsbereich). */
|
||||
export function useParamInAddress(name: string, value: string): void {
|
||||
useEffect(() => {
|
||||
const url = new URL(window.location.href);
|
||||
if (url.searchParams.get(name) === value) return;
|
||||
url.searchParams.set(name, value);
|
||||
window.history.replaceState(window.history.state, '', url);
|
||||
}, [name, value]);
|
||||
}
|
||||
|
||||
export interface ProjectWindow<C> {
|
||||
/** Wechselt mit jedem Projekt; als React-Schluessel des Fensters gedacht. */
|
||||
key: number;
|
||||
@@ -50,9 +74,11 @@ export interface ProjectWindow<C> {
|
||||
|
||||
interface Shown<C> { key: number; project: string; controller: AbortController; carried?: C }
|
||||
|
||||
export function ProjectWindows<C>({ children, around = commit => commit() }: {
|
||||
export function ProjectWindows<C>({ children, around = commit => commit(), keepParams = [] }: {
|
||||
children: (window: ProjectWindow<C>) => ReactNode;
|
||||
around?: (commit: () => void) => void;
|
||||
/** Parameter der ganzen Seite, die ein Wechsel in die neue Adresse mitnimmt (A5). */
|
||||
keepParams?: readonly string[];
|
||||
}): JSX.Element {
|
||||
const [shown, setShown] = useState<Shown<C>>(() => ({ key: 0, project: projectOfLocation(), controller: new AbortController() }));
|
||||
const current = useRef(shown); current.current = shown;
|
||||
@@ -65,9 +91,10 @@ export function ProjectWindows<C>({ children, around = commit => commit() }: {
|
||||
around(() => flushSync(() => setShown(next)));
|
||||
previous.controller.abort();
|
||||
}, [around]);
|
||||
const keep = useRef(keepParams); keep.current = keepParams;
|
||||
const open = useCallback((project: string) => {
|
||||
if (project === current.current.project) return;
|
||||
window.history.pushState(null, '', projectHref(project));
|
||||
window.history.pushState(null, '', projectSwitchHref(project, window.location.search, keep.current));
|
||||
show(project);
|
||||
}, [show]);
|
||||
const report = useCallback((value: C) => { carry.current = value; }, []);
|
||||
|
||||
Reference in new issue
Block a user