Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 180 additions & 1 deletion apps/web/src/components/settings/SettingsPanels.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ import {
type DesktopBridge,
type DesktopUpdateChannel,
type DesktopUpdateState,
type EnvironmentApi,
type LocalApi,
type OrchestrationShellSnapshot,
ProjectId,
ProviderDriverKind,
ProviderInstanceId,
type ServerConfig,
type ServerProcessResourceHistoryResult,
type ServerProvider,
type SourceControlDiscoveryResult,
ThreadId,
} from "@t3tools/contracts";
import * as DateTime from "effect/DateTime";
import * as Option from "effect/Option";
Expand All @@ -30,14 +34,24 @@ import {
createRoute,
createRouter,
} from "@tanstack/react-router";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

import { __resetLocalApiForTests } from "../../localApi";
import {
__resetEnvironmentApiOverridesForTests,
__setEnvironmentApiOverrideForTests,
} from "../../environmentApi";
import { AppAtomRegistryProvider, resetAppAtomRegistryForTests } from "../../rpc/atomRegistry";
import { resetServerStateForTests, setServerConfigSnapshot } from "../../rpc/serverState";
import { useStore } from "../../store";
import { useUiStateStore } from "../../uiStateStore";
import { ConnectionsSettings } from "./ConnectionsSettings";
import { DiagnosticsSettingsPanel } from "./DiagnosticsSettings";
import { GeneralSettingsPanel, ProviderSettingsPanel } from "./SettingsPanels";
import {
ArchivedThreadsPanel,
GeneralSettingsPanel,
ProviderSettingsPanel,
} from "./SettingsPanels";
import { SourceControlSettingsPanel } from "./SourceControlSettings";

function renderWithTestRouter(children: ReactNode) {
Expand Down Expand Up @@ -283,6 +297,71 @@ function createEmptyProcessResourceHistoryResult(): ServerProcessResourceHistory
};
}

const archivedPanelEnvironmentId = EnvironmentId.make("environment-archive-test");
const archivedPanelProjectId = ProjectId.make("project-docs");
const archivedPanelModelSelection = {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5",
};

function createArchivedPanelSnapshot(
threads: OrchestrationShellSnapshot["threads"],
): OrchestrationShellSnapshot {
return {
snapshotSequence: 1,
projects: [
{
id: archivedPanelProjectId,
title: "Docs Portal",
workspaceRoot: "/work/clients/docs",
defaultModelSelection: archivedPanelModelSelection,
scripts: [],
createdAt: "2036-04-07T00:00:00.000Z",
updatedAt: "2036-04-07T00:00:00.000Z",
},
],
threads,
updatedAt: "2036-04-07T00:03:00.000Z",
};
}

function createArchivedPanelThread(input: {
readonly id: string;
readonly title: string;
readonly branch: string | null;
readonly worktreePath: string | null;
readonly createdAt: string;
readonly archivedAt: string;
}): OrchestrationShellSnapshot["threads"][number] {
return {
id: ThreadId.make(input.id),
projectId: archivedPanelProjectId,
title: input.title,
modelSelection: archivedPanelModelSelection,
runtimeMode: "full-access",
interactionMode: "default",
branch: input.branch,
worktreePath: input.worktreePath,
latestTurn: null,
createdAt: input.createdAt,
updatedAt: input.archivedAt,
archivedAt: input.archivedAt,
session: null,
latestUserMessageAt: null,
hasPendingApprovals: false,
hasPendingUserInput: false,
hasActionableProposedPlan: false,
};
}

function createArchivedPanelEnvironmentApi(snapshot: OrchestrationShellSnapshot): EnvironmentApi {
return {
orchestration: {
getArchivedShellSnapshot: vi.fn().mockResolvedValue(snapshot),
},
} as unknown as EnvironmentApi;
}

function makePairingLink(input: {
readonly id: string;
readonly credential: string;
Expand Down Expand Up @@ -1219,6 +1298,106 @@ describe("GeneralSettingsPanel observability", () => {
});
});

describe("ArchivedThreadsPanel", () => {
let mounted:
| (Awaited<ReturnType<typeof render>> & {
cleanup?: () => Promise<void>;
unmount?: () => Promise<void>;
})
| null = null;

beforeEach(() => {
resetAppAtomRegistryForTests();
__resetEnvironmentApiOverridesForTests();
document.body.innerHTML = "";
useStore.setState({
activeEnvironmentId: null,
environmentStateById: {},
});
});

afterEach(async () => {
if (mounted) {
const teardown = mounted.cleanup ?? mounted.unmount;
await teardown?.call(mounted).catch(() => {});
}
mounted = null;
document.body.innerHTML = "";
__resetEnvironmentApiOverridesForTests();
resetAppAtomRegistryForTests();
useStore.setState({
activeEnvironmentId: null,
environmentStateById: {},
});
});

it("searches archived threads and collapses archived projects", async () => {
useStore
.getState()
.syncServerShellSnapshot(createArchivedPanelSnapshot([]), archivedPanelEnvironmentId);

__setEnvironmentApiOverrideForTests(
archivedPanelEnvironmentId,
createArchivedPanelEnvironmentApi(
createArchivedPanelSnapshot([
createArchivedPanelThread({
id: "thread-docs-bug",
title: "Fix publishing bug",
branch: "docs-fix",
worktreePath: "/work/clients/docs/.worktrees/docs-fix",
createdAt: "2036-04-07T00:01:00.000Z",
archivedAt: "2036-04-07T00:04:00.000Z",
}),
createArchivedPanelThread({
id: "thread-docs-homepage",
title: "Rewrite homepage copy",
branch: null,
worktreePath: null,
createdAt: "2036-04-07T00:02:00.000Z",
archivedAt: "2036-04-07T00:03:00.000Z",
}),
]),
),
);

const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});

mounted = await renderWithTestRouter(
<QueryClientProvider client={queryClient}>
<AppAtomRegistryProvider>
<ArchivedThreadsPanel />
</AppAtomRegistryProvider>
</QueryClientProvider>,
);

await expect.element(page.getByText("Fix publishing bug")).toBeInTheDocument();
await expect.element(page.getByText("Rewrite homepage copy")).toBeInTheDocument();

await page.getByLabelText("Search archived threads").fill("homepage");

await expect.element(page.getByText("Rewrite homepage copy")).toBeInTheDocument();
await expect.element(page.getByText("Fix publishing bug")).not.toBeInTheDocument();

await page.getByLabelText("Search archived threads").fill("");
await expect.element(page.getByText("Fix publishing bug")).toBeInTheDocument();

await page.getByRole("button", { name: "Collapse Docs Portal", exact: true }).click();

await expect.element(page.getByText("Fix publishing bug")).not.toBeInTheDocument();
await expect.element(page.getByText("Rewrite homepage copy")).not.toBeInTheDocument();

await page.getByRole("button", { name: "Expand Docs Portal", exact: true }).click();

await expect.element(page.getByText("Fix publishing bug")).toBeInTheDocument();
await expect.element(page.getByText("Rewrite homepage copy")).toBeInTheDocument();
});
});

describe("SourceControlSettingsPanel discovery states", () => {
let mounted:
| (Awaited<ReturnType<typeof render>> & {
Expand Down
91 changes: 91 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,100 @@ import {
import { describe, expect, it } from "vitest";
import {
buildProviderInstanceUpdatePatch,
filterArchivedThreadGroups,
formatDiagnosticsDescription,
} from "./SettingsPanels.logic";

const archivedGroups = [
{
project: {
id: "project-docs",
name: "Docs Portal",
cwd: "/work/clients/docs",
},
threads: [
{
id: "thread-docs-bug",
title: "Fix publishing bug",
branch: "docs-fix",
worktreePath: "/work/clients/docs/.worktrees/docs-fix",
},
{
id: "thread-docs-copy",
title: "Rewrite homepage copy",
branch: null,
worktreePath: null,
},
],
},
{
project: {
id: "project-api",
name: "API Service",
cwd: "/work/services/api",
},
threads: [
{
id: "thread-api-cache",
title: "Tune cache invalidation",
branch: "cache-tuning",
worktreePath: "/work/services/api/.worktrees/cache-tuning",
},
],
},
];

describe("archive thread search helpers", () => {
it("returns all groups for an empty query", () => {
expect(filterArchivedThreadGroups(archivedGroups, " ")).toEqual(archivedGroups);
});

it("keeps all project threads when the project name matches", () => {
const filtered = filterArchivedThreadGroups(archivedGroups, "docs portal");

expect(filtered).toHaveLength(1);
expect(filtered[0]?.project.id).toBe("project-docs");
expect(filtered[0]?.threads.map((thread) => thread.id)).toEqual([
"thread-docs-bug",
"thread-docs-copy",
]);
});

it("keeps all project threads when the project cwd matches", () => {
const filtered = filterArchivedThreadGroups(archivedGroups, "services api");

expect(filtered).toHaveLength(1);
expect(filtered[0]?.project.id).toBe("project-api");
expect(filtered[0]?.threads.map((thread) => thread.id)).toEqual(["thread-api-cache"]);
});

it("keeps only matching threads when the thread title matches", () => {
const filtered = filterArchivedThreadGroups(archivedGroups, " HOMEpage ");

expect(filtered).toHaveLength(1);
expect(filtered[0]?.project.id).toBe("project-docs");
expect(filtered[0]?.threads.map((thread) => thread.id)).toEqual(["thread-docs-copy"]);
});

it("matches thread branch and worktree path", () => {
expect(
filterArchivedThreadGroups(archivedGroups, "cache-tuning")[0]?.threads.map(
(thread) => thread.id,
),
).toEqual(["thread-api-cache"]);

expect(
filterArchivedThreadGroups(archivedGroups, "worktrees docs-fix")[0]?.threads.map(
(thread) => thread.id,
),
).toEqual(["thread-docs-bug"]);
});

it("drops groups with no matches", () => {
expect(filterArchivedThreadGroups(archivedGroups, "not-here")).toEqual([]);
});
});

describe("formatDiagnosticsDescription", () => {
it("collapses trace and metric URLs that share the same OTEL base path", () => {
expect(
Expand Down
68 changes: 68 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,74 @@ import type {
} from "@t3tools/contracts";
import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings";

type ArchivedSearchProject = {
readonly name: string;
readonly cwd: string;
};

type ArchivedSearchThread = {
readonly title: string;
readonly branch: string | null;
readonly worktreePath: string | null;
};

type ArchivedThreadSearchGroup<
TProject extends ArchivedSearchProject = ArchivedSearchProject,
TThread extends ArchivedSearchThread = ArchivedSearchThread,
> = {
readonly project: TProject;
readonly threads: ReadonlyArray<TThread>;
};

function normalizeArchivedSearchQuery(query: string): string[] {
return query.trim().toLowerCase().split(/\s+/).filter(Boolean);
}

function searchableTextMatchesAllTokens(
fields: ReadonlyArray<string | null | undefined>,
tokens: ReadonlyArray<string>,
): boolean {
if (tokens.length === 0) {
return true;
}

const searchableText = fields
.filter((field): field is string => typeof field === "string" && field.length > 0)
.join(" ")
.toLowerCase();

return tokens.every((token) => searchableText.includes(token));
}

export function filterArchivedThreadGroups<
TProject extends ArchivedSearchProject,
TThread extends ArchivedSearchThread,
>(
groups: ReadonlyArray<ArchivedThreadSearchGroup<TProject, TThread>>,
query: string,
): Array<ArchivedThreadSearchGroup<TProject, TThread>> {
const tokens = normalizeArchivedSearchQuery(query);
if (tokens.length === 0) {
return [...groups];
}

return groups.flatMap((group) => {
const projectFields = [group.project.name, group.project.cwd];
if (searchableTextMatchesAllTokens(projectFields, tokens)) {
return [group];
}

const threads = group.threads.filter((thread) =>
searchableTextMatchesAllTokens(
[...projectFields, thread.title, thread.branch, thread.worktreePath],
tokens,
),
);

return threads.length > 0 ? [{ ...group, threads }] : [];
});
}

function collapseOtelSignalsUrl(input: {
readonly tracesUrl: string;
readonly metricsUrl: string;
Expand Down
Loading
Loading