mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
feat(goose2): add useActiveProjectTint hook
Pure selector: active session → project → color (or null). Route gating lives at the application point in AppShell, not here. Unit-tested against four cases: no active session, no project on session, in- project, deleted project. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { useChatSessionStore } from "@/features/chat/stores/chatSessionStore";
|
||||
import { useProjectStore } from "@/features/projects/stores/projectStore";
|
||||
|
||||
import { useActiveProjectTint } from "../useActiveProjectTint";
|
||||
|
||||
const baseSessionState = {
|
||||
sessions: [],
|
||||
activeSessionId: null,
|
||||
isLoading: false,
|
||||
hasHydratedSessions: true,
|
||||
contextPanelOpenBySession: {},
|
||||
activeWorkspaceBySession: {},
|
||||
};
|
||||
|
||||
const baseProjectState = {
|
||||
projects: [],
|
||||
loading: false,
|
||||
activeProjectId: null,
|
||||
};
|
||||
|
||||
describe("useActiveProjectTint", () => {
|
||||
beforeEach(() => {
|
||||
useChatSessionStore.setState(baseSessionState);
|
||||
useProjectStore.setState(baseProjectState);
|
||||
});
|
||||
|
||||
it("returns null when there is no active session", () => {
|
||||
const { result } = renderHook(() => useActiveProjectTint());
|
||||
expect(result.current).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when the active session has no projectId", () => {
|
||||
useChatSessionStore.setState({
|
||||
...baseSessionState,
|
||||
sessions: [
|
||||
{
|
||||
id: "s1",
|
||||
title: "Chat",
|
||||
providerId: "openai",
|
||||
modelId: "gpt-4o",
|
||||
modelName: "GPT-4o",
|
||||
createdAt: "2026-04-28T00:00:00.000Z",
|
||||
updatedAt: "2026-04-28T00:00:00.000Z",
|
||||
messageCount: 0,
|
||||
},
|
||||
],
|
||||
activeSessionId: "s1",
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useActiveProjectTint());
|
||||
expect(result.current).toBeNull();
|
||||
});
|
||||
|
||||
it("returns the project's color when the active session is in-project", () => {
|
||||
useProjectStore.setState({
|
||||
...baseProjectState,
|
||||
projects: [
|
||||
{
|
||||
id: "p1",
|
||||
name: "Blue project",
|
||||
color: "#3b82f6",
|
||||
description: "",
|
||||
prompt: "",
|
||||
icon: "",
|
||||
preferredProvider: null,
|
||||
preferredModel: null,
|
||||
workingDirs: [],
|
||||
useWorktrees: false,
|
||||
order: 0,
|
||||
archivedAt: null,
|
||||
createdAt: "2026-04-28T00:00:00.000Z",
|
||||
updatedAt: "2026-04-28T00:00:00.000Z",
|
||||
artifactsDir: "",
|
||||
},
|
||||
],
|
||||
});
|
||||
useChatSessionStore.setState({
|
||||
...baseSessionState,
|
||||
sessions: [
|
||||
{
|
||||
id: "s1",
|
||||
title: "Chat",
|
||||
providerId: "openai",
|
||||
modelId: "gpt-4o",
|
||||
modelName: "GPT-4o",
|
||||
createdAt: "2026-04-28T00:00:00.000Z",
|
||||
updatedAt: "2026-04-28T00:00:00.000Z",
|
||||
messageCount: 0,
|
||||
projectId: "p1",
|
||||
},
|
||||
],
|
||||
activeSessionId: "s1",
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useActiveProjectTint());
|
||||
expect(result.current).toBe("#3b82f6");
|
||||
});
|
||||
|
||||
it("returns null when the active session references a deleted project", () => {
|
||||
useChatSessionStore.setState({
|
||||
...baseSessionState,
|
||||
sessions: [
|
||||
{
|
||||
id: "s1",
|
||||
title: "Chat",
|
||||
providerId: "openai",
|
||||
modelId: "gpt-4o",
|
||||
modelName: "GPT-4o",
|
||||
createdAt: "2026-04-28T00:00:00.000Z",
|
||||
updatedAt: "2026-04-28T00:00:00.000Z",
|
||||
messageCount: 0,
|
||||
projectId: "ghost-project",
|
||||
},
|
||||
],
|
||||
activeSessionId: "s1",
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useActiveProjectTint());
|
||||
expect(result.current).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useChatSessionStore } from "@/features/chat/stores/chatSessionStore";
|
||||
import { useProjectStore } from "@/features/projects/stores/projectStore";
|
||||
|
||||
/**
|
||||
* Resolves the active chat session's project color. Returns the hex string
|
||||
* if the active session is bound to a known project, otherwise null. The
|
||||
* route gate (only-tint-when-on-chat-view) lives at the application point
|
||||
* in AppShell, not here — this hook stays a pure session→project→color
|
||||
* selector so it has one named place to test.
|
||||
*/
|
||||
export function useActiveProjectTint(): string | null {
|
||||
const activeSessionId = useChatSessionStore((s) => s.activeSessionId);
|
||||
const sessions = useChatSessionStore((s) => s.sessions);
|
||||
const projects = useProjectStore((s) => s.projects);
|
||||
|
||||
if (!activeSessionId) return null;
|
||||
const session = sessions.find((s) => s.id === activeSessionId);
|
||||
if (!session?.projectId) return null;
|
||||
const project = projects.find((p) => p.id === session.projectId);
|
||||
return project?.color ?? null;
|
||||
}
|
||||
Reference in New Issue
Block a user