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:
tulsi
2026-04-28 15:38:44 -07:00
parent cb9b37297d
commit 8b1666f9f7
2 changed files with 145 additions and 0 deletions
@@ -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;
}