Preserve selected branch across project chats (#9010)
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> Signed-off-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
@@ -47,6 +47,7 @@ import { perfLog } from "@/shared/lib/perfLog";
|
|||||||
import { useProviderInventoryStore } from "@/features/providers/stores/providerInventoryStore";
|
import { useProviderInventoryStore } from "@/features/providers/stores/providerInventoryStore";
|
||||||
import type { SkillInfo } from "@/features/skills/api/skills";
|
import type { SkillInfo } from "@/features/skills/api/skills";
|
||||||
import { toChatSkillDraft } from "@/features/skills/lib/skillChatPrompt";
|
import { toChatSkillDraft } from "@/features/skills/lib/skillChatPrompt";
|
||||||
|
import { resolveInheritedProjectWorkspace } from "@/features/chat/lib/workspaceContext";
|
||||||
import { OnboardingFlow } from "@/features/onboarding/ui/OnboardingFlow";
|
import { OnboardingFlow } from "@/features/onboarding/ui/OnboardingFlow";
|
||||||
import { useOnboardingGate } from "@/features/onboarding/hooks/useOnboardingGate";
|
import { useOnboardingGate } from "@/features/onboarding/hooks/useOnboardingGate";
|
||||||
import { Spinner } from "@/shared/ui/spinner";
|
import { Spinner } from "@/shared/ui/spinner";
|
||||||
@@ -166,6 +167,7 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
|||||||
const createSession = useChatSessionStore((s) => s.createSession);
|
const createSession = useChatSessionStore((s) => s.createSession);
|
||||||
const patchSession = useChatSessionStore((s) => s.patchSession);
|
const patchSession = useChatSessionStore((s) => s.patchSession);
|
||||||
const setActiveSession = useChatSessionStore((s) => s.setActiveSession);
|
const setActiveSession = useChatSessionStore((s) => s.setActiveSession);
|
||||||
|
const setActiveWorkspace = useChatSessionStore((s) => s.setActiveWorkspace);
|
||||||
const archiveSession = useChatSessionStore((s) => s.archiveSession);
|
const archiveSession = useChatSessionStore((s) => s.archiveSession);
|
||||||
const selectedProvider = useAgentStore(selectSelectedProvider);
|
const selectedProvider = useAgentStore(selectSelectedProvider);
|
||||||
const projects = useProjectStore(selectProjects);
|
const projects = useProjectStore(selectProjects);
|
||||||
@@ -205,7 +207,14 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
|||||||
.getState()
|
.getState()
|
||||||
.projects.find((p) => p.id === session.projectId) ?? null)
|
.projects.find((p) => p.id === session.projectId) ?? null)
|
||||||
: null;
|
: null;
|
||||||
const workingDir = await resolveSessionCwd(project);
|
const activeWorkspace =
|
||||||
|
session?.id != null
|
||||||
|
? useChatSessionStore.getState().activeWorkspaceBySession[session.id]
|
||||||
|
: undefined;
|
||||||
|
const workingDir = await resolveSessionCwd(
|
||||||
|
project,
|
||||||
|
activeWorkspace?.path ?? session?.workingDir,
|
||||||
|
);
|
||||||
await acpLoadSession(sessionId, workingDir);
|
await acpLoadSession(sessionId, workingDir);
|
||||||
const tFlush = performance.now();
|
const tFlush = performance.now();
|
||||||
useChatStore.getState().setSessionLoading(sessionId, false);
|
useChatStore.getState().setSessionLoading(sessionId, false);
|
||||||
@@ -411,6 +420,12 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
|||||||
);
|
);
|
||||||
const sessionState = useChatSessionStore.getState();
|
const sessionState = useChatSessionStore.getState();
|
||||||
const chatState = useChatStore.getState();
|
const chatState = useChatStore.getState();
|
||||||
|
const inheritedWorkspace = resolveInheritedProjectWorkspace({
|
||||||
|
projectId: project?.id,
|
||||||
|
sessions: sessionState.sessions,
|
||||||
|
activeSessionId: sessionState.activeSessionId,
|
||||||
|
activeWorkspaceBySession: sessionState.activeWorkspaceBySession,
|
||||||
|
});
|
||||||
const existingDraft = findExistingDraft({
|
const existingDraft = findExistingDraft({
|
||||||
sessions: sessionState.sessions,
|
sessions: sessionState.sessions,
|
||||||
activeSessionId: sessionState.activeSessionId,
|
activeSessionId: sessionState.activeSessionId,
|
||||||
@@ -423,6 +438,12 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (existingDraft) {
|
if (existingDraft) {
|
||||||
|
if (inheritedWorkspace) {
|
||||||
|
setActiveWorkspace(existingDraft.id, inheritedWorkspace);
|
||||||
|
patchSession(existingDraft.id, {
|
||||||
|
workingDir: inheritedWorkspace.path,
|
||||||
|
});
|
||||||
|
}
|
||||||
setActiveSession(existingDraft.id);
|
setActiveSession(existingDraft.id);
|
||||||
clearSettingsSectionUrl();
|
clearSettingsSectionUrl();
|
||||||
setActiveView("chat");
|
setActiveView("chat");
|
||||||
@@ -433,7 +454,10 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
|||||||
return existingDraft;
|
return existingDraft;
|
||||||
}
|
}
|
||||||
|
|
||||||
const workingDir = await resolveSessionCwd(project);
|
const workingDir = await resolveSessionCwd(
|
||||||
|
project,
|
||||||
|
inheritedWorkspace?.path,
|
||||||
|
);
|
||||||
const session = await createSession({
|
const session = await createSession({
|
||||||
title,
|
title,
|
||||||
projectId: project?.id,
|
projectId: project?.id,
|
||||||
@@ -442,6 +466,9 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
|||||||
modelId: sessionModelPreference.modelId,
|
modelId: sessionModelPreference.modelId,
|
||||||
modelName: sessionModelPreference.modelName,
|
modelName: sessionModelPreference.modelName,
|
||||||
});
|
});
|
||||||
|
if (inheritedWorkspace) {
|
||||||
|
setActiveWorkspace(session.id, inheritedWorkspace);
|
||||||
|
}
|
||||||
setActiveSession(session.id);
|
setActiveSession(session.id);
|
||||||
clearSettingsSectionUrl();
|
clearSettingsSectionUrl();
|
||||||
setActiveView("chat");
|
setActiveView("chat");
|
||||||
@@ -454,8 +481,10 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
|||||||
[
|
[
|
||||||
selectedProvider,
|
selectedProvider,
|
||||||
createSession,
|
createSession,
|
||||||
|
patchSession,
|
||||||
providerInventoryEntries,
|
providerInventoryEntries,
|
||||||
setActiveSession,
|
setActiveSession,
|
||||||
|
setActiveWorkspace,
|
||||||
setChatActiveSession,
|
setChatActiveSession,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -639,6 +668,7 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
|||||||
workingDir,
|
workingDir,
|
||||||
modelId: session.modelId,
|
modelId: session.modelId,
|
||||||
});
|
});
|
||||||
|
patchSession(sessionId, { workingDir });
|
||||||
})().catch((error) => {
|
})().catch((error) => {
|
||||||
console.error(
|
console.error(
|
||||||
"Failed to update ACP session project working directory:",
|
"Failed to update ACP session project working directory:",
|
||||||
@@ -646,7 +676,7 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[selectedProvider],
|
[selectedProvider, patchSession],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRenameChat = useCallback(
|
const handleRenameChat = useCallback(
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ describe("useChat attachments", () => {
|
|||||||
sessions: [],
|
sessions: [],
|
||||||
activeSessionId: null,
|
activeSessionId: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
isContextPanelOpen: false,
|
||||||
contextPanelOpenBySession: {},
|
contextPanelOpenBySession: {},
|
||||||
activeWorkspaceBySession: {},
|
activeWorkspaceBySession: {},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ describe("useChat skill chips", () => {
|
|||||||
sessions: [],
|
sessions: [],
|
||||||
activeSessionId: null,
|
activeSessionId: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
isContextPanelOpen: false,
|
||||||
contextPanelOpenBySession: {},
|
contextPanelOpenBySession: {},
|
||||||
activeWorkspaceBySession: {},
|
activeWorkspaceBySession: {},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ describe("useChat", () => {
|
|||||||
sessions: [],
|
sessions: [],
|
||||||
activeSessionId: null,
|
activeSessionId: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
isContextPanelOpen: false,
|
||||||
contextPanelOpenBySession: {},
|
contextPanelOpenBySession: {},
|
||||||
activeWorkspaceBySession: {},
|
activeWorkspaceBySession: {},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ describe("useChatSessionController", () => {
|
|||||||
activeSessionId: null,
|
activeSessionId: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
hasHydratedSessions: true,
|
hasHydratedSessions: true,
|
||||||
|
isContextPanelOpen: false,
|
||||||
contextPanelOpenBySession: {},
|
contextPanelOpenBySession: {},
|
||||||
activeWorkspaceBySession: {},
|
activeWorkspaceBySession: {},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -369,11 +369,13 @@ export function useChat(
|
|||||||
setPendingAssistantProvider,
|
setPendingAssistantProvider,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const getWorkingDir = useCallback(
|
const getWorkingDir = useCallback(() => {
|
||||||
() =>
|
const sessionStore = useChatSessionStore.getState();
|
||||||
useChatSessionStore.getState().activeWorkspaceBySession[sessionId]?.path,
|
return (
|
||||||
[sessionId],
|
sessionStore.activeWorkspaceBySession[sessionId]?.path ??
|
||||||
);
|
sessionStore.getSession(sessionId)?.workingDir
|
||||||
|
);
|
||||||
|
}, [sessionId]);
|
||||||
|
|
||||||
const compactConversation = useCallback(
|
const compactConversation = useCallback(
|
||||||
async (overridePersona?: { id: string; name?: string }) => {
|
async (overridePersona?: { id: string; name?: string }) => {
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ export function useChatSessionController({
|
|||||||
workingDir,
|
workingDir,
|
||||||
modelId: modelSelection?.id,
|
modelId: modelSelection?.id,
|
||||||
});
|
});
|
||||||
|
useChatSessionStore.getState().patchSession(sessionId, { workingDir });
|
||||||
if (!result.applied || !modelSelection?.id) {
|
if (!result.applied || !modelSelection?.id) {
|
||||||
return result.applied;
|
return result.applied;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { ChatSession } from "../stores/chatSessionStore";
|
||||||
|
import { resolveInheritedProjectWorkspace } from "./workspaceContext";
|
||||||
|
|
||||||
|
function makeSession(overrides: Partial<ChatSession> = {}): ChatSession {
|
||||||
|
return {
|
||||||
|
id: "session-1",
|
||||||
|
title: "New chat",
|
||||||
|
projectId: "project-1",
|
||||||
|
workingDir: "/repo/main",
|
||||||
|
createdAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
messageCount: 0,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("resolveInheritedProjectWorkspace", () => {
|
||||||
|
it("inherits the active workspace for another chat in the same project", () => {
|
||||||
|
expect(
|
||||||
|
resolveInheritedProjectWorkspace({
|
||||||
|
projectId: "project-1",
|
||||||
|
sessions: [makeSession()],
|
||||||
|
activeSessionId: "session-1",
|
||||||
|
activeWorkspaceBySession: {
|
||||||
|
"session-1": { path: "/repo/feature", branch: "feature" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual({ path: "/repo/feature", branch: "feature" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to the active session working directory after reload", () => {
|
||||||
|
expect(
|
||||||
|
resolveInheritedProjectWorkspace({
|
||||||
|
projectId: "project-1",
|
||||||
|
sessions: [makeSession({ workingDir: "/repo/feature" })],
|
||||||
|
activeSessionId: "session-1",
|
||||||
|
activeWorkspaceBySession: {},
|
||||||
|
}),
|
||||||
|
).toEqual({ path: "/repo/feature", branch: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not inherit workspace across projects", () => {
|
||||||
|
expect(
|
||||||
|
resolveInheritedProjectWorkspace({
|
||||||
|
projectId: "project-2",
|
||||||
|
sessions: [makeSession()],
|
||||||
|
activeSessionId: "session-1",
|
||||||
|
activeWorkspaceBySession: {
|
||||||
|
"session-1": { path: "/repo/feature", branch: "feature" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import type {
|
||||||
|
ActiveWorkspace,
|
||||||
|
ChatSession,
|
||||||
|
} from "@/features/chat/stores/chatSessionStore";
|
||||||
|
|
||||||
|
interface ResolveInheritedProjectWorkspaceOptions {
|
||||||
|
projectId: string | undefined;
|
||||||
|
sessions: ChatSession[];
|
||||||
|
activeSessionId: string | null;
|
||||||
|
activeWorkspaceBySession: Record<string, ActiveWorkspace>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveInheritedProjectWorkspace({
|
||||||
|
projectId,
|
||||||
|
sessions,
|
||||||
|
activeSessionId,
|
||||||
|
activeWorkspaceBySession,
|
||||||
|
}: ResolveInheritedProjectWorkspaceOptions): ActiveWorkspace | undefined {
|
||||||
|
if (!projectId || !activeSessionId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeSession = sessions.find(
|
||||||
|
(session) => session.id === activeSessionId,
|
||||||
|
);
|
||||||
|
if (activeSession?.projectId !== projectId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeWorkspace = activeWorkspaceBySession[activeSessionId];
|
||||||
|
if (activeWorkspace?.path) {
|
||||||
|
return activeWorkspace;
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeSession.workingDir
|
||||||
|
? { path: activeSession.workingDir, branch: null }
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ function resetStore() {
|
|||||||
activeSessionId: null,
|
activeSessionId: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
hasHydratedSessions: false,
|
hasHydratedSessions: false,
|
||||||
|
isContextPanelOpen: false,
|
||||||
contextPanelOpenBySession: {},
|
contextPanelOpenBySession: {},
|
||||||
activeWorkspaceBySession: {},
|
activeWorkspaceBySession: {},
|
||||||
});
|
});
|
||||||
@@ -49,6 +50,7 @@ function seedSession(overrides: Partial<ChatSession> = {}): ChatSession {
|
|||||||
|
|
||||||
describe("chatSessionStore", () => {
|
describe("chatSessionStore", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
window.localStorage.removeItem("goose:context-panel-open");
|
||||||
resetStore();
|
resetStore();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
@@ -271,6 +273,20 @@ describe("chatSessionStore", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("context panel preference", () => {
|
||||||
|
it("stores context panel open state as a global preference", () => {
|
||||||
|
useChatSessionStore.getState().setContextPanelOpen("session-1", true);
|
||||||
|
|
||||||
|
expect(useChatSessionStore.getState().isContextPanelOpen).toBe(true);
|
||||||
|
expect(window.localStorage.getItem("goose:context-panel-open")).toBe("1");
|
||||||
|
|
||||||
|
useChatSessionStore.getState().setContextPanelOpen("session-2", false);
|
||||||
|
|
||||||
|
expect(useChatSessionStore.getState().isContextPanelOpen).toBe(false);
|
||||||
|
expect(window.localStorage.getItem("goose:context-panel-open")).toBe("0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("archiveSession", () => {
|
describe("archiveSession", () => {
|
||||||
it("sets archivedAt on the session", async () => {
|
it("sets archivedAt on the session", async () => {
|
||||||
const session = seedSession();
|
const session = seedSession();
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import {
|
|||||||
unarchiveSession as acpUnarchiveSession,
|
unarchiveSession as acpUnarchiveSession,
|
||||||
} from "@/shared/api/acpApi";
|
} from "@/shared/api/acpApi";
|
||||||
|
|
||||||
|
const CONTEXT_PANEL_OPEN_STORAGE_KEY = "goose:context-panel-open";
|
||||||
|
|
||||||
export interface ChatSession {
|
export interface ChatSession {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -58,6 +60,7 @@ interface ChatSessionStoreState {
|
|||||||
activeSessionId: string | null;
|
activeSessionId: string | null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
hasHydratedSessions: boolean;
|
hasHydratedSessions: boolean;
|
||||||
|
isContextPanelOpen: boolean;
|
||||||
contextPanelOpenBySession: Record<string, boolean>;
|
contextPanelOpenBySession: Record<string, boolean>;
|
||||||
activeWorkspaceBySession: Record<string, ActiveWorkspace>;
|
activeWorkspaceBySession: Record<string, ActiveWorkspace>;
|
||||||
}
|
}
|
||||||
@@ -116,6 +119,29 @@ function sortByUpdatedAtDesc(sessions: ChatSession[]): ChatSession[] {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadContextPanelOpenPreference(): boolean {
|
||||||
|
if (typeof window === "undefined") return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return window.localStorage.getItem(CONTEXT_PANEL_OPEN_STORAGE_KEY) === "1";
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistContextPanelOpenPreference(open: boolean): void {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
CONTEXT_PANEL_OPEN_STORAGE_KEY,
|
||||||
|
open ? "1" : "0",
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// localStorage may be unavailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function sessionToChatSession(session: Session): ChatSession {
|
export function sessionToChatSession(session: Session): ChatSession {
|
||||||
return {
|
return {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
@@ -138,6 +164,7 @@ export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
|
|||||||
activeSessionId: null,
|
activeSessionId: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
hasHydratedSessions: false,
|
hasHydratedSessions: false,
|
||||||
|
isContextPanelOpen: loadContextPanelOpenPreference(),
|
||||||
contextPanelOpenBySession: {},
|
contextPanelOpenBySession: {},
|
||||||
activeWorkspaceBySession: {},
|
activeWorkspaceBySession: {},
|
||||||
|
|
||||||
@@ -255,13 +282,9 @@ export const useChatSessionStore = create<ChatSessionStore>((set, get) => ({
|
|||||||
set({ activeSessionId: sessionId });
|
set({ activeSessionId: sessionId });
|
||||||
},
|
},
|
||||||
|
|
||||||
setContextPanelOpen: (sessionId, open) => {
|
setContextPanelOpen: (_sessionId, open) => {
|
||||||
set((state) => ({
|
persistContextPanelOpenPreference(open);
|
||||||
contextPanelOpenBySession: {
|
set({ isContextPanelOpen: open, contextPanelOpenBySession: {} });
|
||||||
...state.contextPanelOpenBySession,
|
|
||||||
[sessionId]: open,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setActiveWorkspace: (sessionId, context) => {
|
setActiveWorkspace: (sessionId, context) => {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ interface ChatContextPanelProps {
|
|||||||
color?: string;
|
color?: string;
|
||||||
workingDirs?: string[];
|
workingDirs?: string[];
|
||||||
} | null;
|
} | null;
|
||||||
|
sessionWorkingDir?: string | null;
|
||||||
setOpen: (sessionId: string, open: boolean) => void;
|
setOpen: (sessionId: string, open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ export function ChatContextPanel({
|
|||||||
isOpen,
|
isOpen,
|
||||||
label,
|
label,
|
||||||
project,
|
project,
|
||||||
|
sessionWorkingDir,
|
||||||
setOpen,
|
setOpen,
|
||||||
}: ChatContextPanelProps) {
|
}: ChatContextPanelProps) {
|
||||||
const shouldReduceMotion = useReducedMotion();
|
const shouldReduceMotion = useReducedMotion();
|
||||||
@@ -104,6 +106,7 @@ export function ChatContextPanel({
|
|||||||
projectName={project?.name}
|
projectName={project?.name}
|
||||||
projectColor={project?.color}
|
projectColor={project?.color}
|
||||||
projectWorkingDirs={project?.workingDirs ?? []}
|
projectWorkingDirs={project?.workingDirs ?? []}
|
||||||
|
sessionWorkingDir={sessionWorkingDir}
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -47,9 +47,7 @@ export function ChatView({
|
|||||||
}: ChatViewProps) {
|
}: ChatViewProps) {
|
||||||
const { t } = useTranslation("chat");
|
const { t } = useTranslation("chat");
|
||||||
const mountStart = useRef(performance.now());
|
const mountStart = useRef(performance.now());
|
||||||
const isContextPanelOpen = useChatSessionStore(
|
const isContextPanelOpen = useChatSessionStore((s) => s.isContextPanelOpen);
|
||||||
(s) => s.contextPanelOpenBySession[sessionId] ?? false,
|
|
||||||
);
|
|
||||||
const setContextPanelOpen = useChatSessionStore((s) => s.setContextPanelOpen);
|
const setContextPanelOpen = useChatSessionStore((s) => s.setContextPanelOpen);
|
||||||
const [isLoadingIndicatorMounted, setIsLoadingIndicatorMounted] =
|
const [isLoadingIndicatorMounted, setIsLoadingIndicatorMounted] =
|
||||||
useState(false);
|
useState(false);
|
||||||
@@ -189,6 +187,7 @@ export function ChatView({
|
|||||||
isOpen={isContextPanelOpen}
|
isOpen={isContextPanelOpen}
|
||||||
label={contextPanelLabel}
|
label={contextPanelLabel}
|
||||||
project={controller.project}
|
project={controller.project}
|
||||||
|
sessionWorkingDir={controller.session?.workingDir}
|
||||||
setOpen={setContextPanelOpen}
|
setOpen={setContextPanelOpen}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ interface ContextPanelProps {
|
|||||||
projectName?: string;
|
projectName?: string;
|
||||||
projectColor?: string;
|
projectColor?: string;
|
||||||
projectWorkingDirs?: string[];
|
projectWorkingDirs?: string[];
|
||||||
|
sessionWorkingDir?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContextPanelTab = "details" | "files";
|
type ContextPanelTab = "details" | "files";
|
||||||
@@ -64,6 +65,7 @@ export function ContextPanel({
|
|||||||
projectName,
|
projectName,
|
||||||
projectColor,
|
projectColor,
|
||||||
projectWorkingDirs = [],
|
projectWorkingDirs = [],
|
||||||
|
sessionWorkingDir,
|
||||||
}: ContextPanelProps) {
|
}: ContextPanelProps) {
|
||||||
const { t } = useTranslation("chat");
|
const { t } = useTranslation("chat");
|
||||||
const [activeTab, setActiveTab] = useState<ContextPanelTab>("details");
|
const [activeTab, setActiveTab] = useState<ContextPanelTab>("details");
|
||||||
@@ -77,7 +79,8 @@ export function ContextPanel({
|
|||||||
);
|
);
|
||||||
const setActiveWorkspace = useChatSessionStore((s) => s.setActiveWorkspace);
|
const setActiveWorkspace = useChatSessionStore((s) => s.setActiveWorkspace);
|
||||||
|
|
||||||
const gitTargetPath = activeContext?.path ?? primaryWorkspaceRoot;
|
const gitTargetPath =
|
||||||
|
activeContext?.path ?? sessionWorkingDir ?? primaryWorkspaceRoot;
|
||||||
const {
|
const {
|
||||||
data: gitState,
|
data: gitState,
|
||||||
error,
|
error,
|
||||||
@@ -230,6 +233,7 @@ export function ContextPanel({
|
|||||||
projectName={projectName}
|
projectName={projectName}
|
||||||
projectColor={projectColor}
|
projectColor={projectColor}
|
||||||
projectWorkingDirs={projectWorkingDirs}
|
projectWorkingDirs={projectWorkingDirs}
|
||||||
|
sessionWorkingDir={sessionWorkingDir}
|
||||||
gitState={gitState}
|
gitState={gitState}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
isFetching={isFetching}
|
isFetching={isFetching}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ vi.mock("../../hooks/useChatSessionController", () => ({
|
|||||||
vi.mock("../../stores/chatSessionStore", () => ({
|
vi.mock("../../stores/chatSessionStore", () => ({
|
||||||
useChatSessionStore: (selector: (state: unknown) => unknown) =>
|
useChatSessionStore: (selector: (state: unknown) => unknown) =>
|
||||||
selector({
|
selector({
|
||||||
|
isContextPanelOpen: false,
|
||||||
contextPanelOpenBySession: {},
|
contextPanelOpenBySession: {},
|
||||||
setContextPanelOpen: vi.fn(),
|
setContextPanelOpen: vi.fn(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ interface WorkspaceWidgetProps {
|
|||||||
projectName?: string;
|
projectName?: string;
|
||||||
projectColor?: string;
|
projectColor?: string;
|
||||||
projectWorkingDirs: string[];
|
projectWorkingDirs: string[];
|
||||||
|
sessionWorkingDir?: string | null;
|
||||||
gitState: GitState | undefined;
|
gitState: GitState | undefined;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
@@ -44,6 +45,7 @@ export function WorkspaceWidget({
|
|||||||
projectName,
|
projectName,
|
||||||
projectColor,
|
projectColor,
|
||||||
projectWorkingDirs,
|
projectWorkingDirs,
|
||||||
|
sessionWorkingDir,
|
||||||
gitState,
|
gitState,
|
||||||
isLoading,
|
isLoading,
|
||||||
isFetching,
|
isFetching,
|
||||||
@@ -62,7 +64,8 @@ export function WorkspaceWidget({
|
|||||||
onToggleOpen,
|
onToggleOpen,
|
||||||
}: WorkspaceWidgetProps) {
|
}: WorkspaceWidgetProps) {
|
||||||
const { t } = useTranslation("chat");
|
const { t } = useTranslation("chat");
|
||||||
const primaryWorkspaceRoot = projectWorkingDirs[0] ?? null;
|
const primaryWorkspaceRoot =
|
||||||
|
activeContext?.path ?? sessionWorkingDir ?? projectWorkingDirs[0] ?? null;
|
||||||
|
|
||||||
const gitErrorMessage =
|
const gitErrorMessage =
|
||||||
error instanceof Error ? error.message : t("contextPanel.errors.gitRead");
|
error instanceof Error ? error.message : t("contextPanel.errors.gitRead");
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ describe("ACP session info updates", () => {
|
|||||||
activeSessionId: null,
|
activeSessionId: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
hasHydratedSessions: false,
|
hasHydratedSessions: false,
|
||||||
|
isContextPanelOpen: false,
|
||||||
contextPanelOpenBySession: {},
|
contextPanelOpenBySession: {},
|
||||||
activeWorkspaceBySession: {},
|
activeWorkspaceBySession: {},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user