Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 1 addition & 3 deletions apps/web/src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -622,9 +622,7 @@ function OpenCommandPaletteDialog(props: {
const currentProjectEnvironmentId =
activeThread?.environmentId ?? activeDraftThread?.environmentId ?? null;
const currentProjectId = activeThread?.projectId ?? activeDraftThread?.projectId ?? null;
const scratchTargetEnvironmentId = scratchEnvironmentId(
currentProjectEnvironmentId ?? primaryEnvironmentId,
);
const scratchTargetEnvironmentId = scratchEnvironmentId(currentProjectEnvironmentId);
const supportsTriage = (environmentId: EnvironmentId | null) =>
environments.find((environment) => environment.environmentId === environmentId)?.serverConfig
?.environment.capabilities.triage === true;
Expand Down
7 changes: 3 additions & 4 deletions apps/web/src/components/chat/DraftHeroHeadline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,11 @@ export function DraftHeroHeadline({
project.environmentId === activeProjectRef.environmentId &&
project.id === activeProjectRef.projectId,
) ?? null);
const scratchTargetEnvironmentId = scratchEnvironmentId(
activeProjectRef?.environmentId ?? primaryEnvironmentId,
);
const scratchTargetEnvironmentId = scratchEnvironmentId(activeProjectRef?.environmentId ?? null);
const scratchWorkspaceRoot = scratchWorkspaceRootFor(scratchTargetEnvironmentId);
const isScratchDraft =
activeProject !== null && isScratchProject(activeProject, scratchWorkspaceRoot);
activeProject !== null &&
isScratchProject(activeProject, scratchWorkspaceRootFor(activeProject.environmentId));

const pickerItems: readonly PickerItem[] = [
...menuEntries.map(({ group }) => ({ value: group.projectKey, label: group.displayName })),
Expand Down
3 changes: 1 addition & 2 deletions apps/web/src/components/promptLinks/NewThreadLinkView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,7 @@ export function NewThreadLinkView({ link }: { readonly link: NewThreadLink }) {
};

const openScratchDraft = async () => {
const environmentId =
scratchEnvironmentId(lastActiveEnvironmentId()) ?? scratchEnvironmentId(null);
const environmentId = scratchEnvironmentId(lastActiveEnvironmentId());
const project = environmentId === null ? null : await openScratchProject(environmentId);
if (project === null) {
setState({
Expand Down
44 changes: 44 additions & 0 deletions apps/web/src/hooks/useScratchProject.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { EnvironmentId } from "@t3tools/contracts";
import { AtomRegistry } from "effect/reactivity";
import { describe, expect, it } from "vite-plus/test";

import { createScratchProjectActions } from "./useScratchProject";

const local = "local" as EnvironmentId;
const remote = "remote" as EnvironmentId;
const other = "other" as EnvironmentId;

function scratchEnvironment(environmentId: EnvironmentId, scratchWorkspaceRoot: string | null) {
return {
environmentId,
connection: { phase: "connected" },
serverConfig: { scratchWorkspaceRoot },
} as never;
}

function actions(primary: EnvironmentId | null, localRoot: string | null) {
const environments = [
scratchEnvironment(local, localRoot),
scratchEnvironment(remote, "/remote/scratch"),
scratchEnvironment(other, "/other/scratch"),
];
return createScratchProjectActions({
environments: () => environments,
primaryEnvironmentId: () => primary,
registry: AtomRegistry.make(),
handleNewThread: async () => null,
});
}

describe("scratchEnvironmentId", () => {
it("prefers the primary environment over the one on screen", () => {
expect(actions(local, "/local/scratch").scratchEnvironmentId(remote)).toBe(local);
expect(actions(local, "/local/scratch").scratchEnvironmentId(null)).toBe(local);
});

it("falls back when the primary environment offers no scratch root", () => {
expect(actions(local, null).scratchEnvironmentId(remote)).toBe(remote);
expect(actions(null, null).scratchEnvironmentId(remote)).toBe(remote);
expect(actions(local, null).scratchEnvironmentId(null)).toBeNull();
});
});
16 changes: 13 additions & 3 deletions apps/web/src/hooks/useScratchProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import type { AtomRegistry } from "effect/reactivity";
import { useContext } from "react";

import { stackedThreadToast, toastManager } from "~/components/ui/toast";
import { type EnvironmentPresentation, useEnvironments } from "~/state/environments";
import {
type EnvironmentPresentation,
useEnvironments,
usePrimaryEnvironmentId,
} from "~/state/environments";
import { projectEnvironment } from "~/state/projects";
import { type NewThreadHandler, useNewThreadHandler } from "./useHandleNewThread";

Expand All @@ -32,6 +36,7 @@ type ScratchEnvironment = Pick<EnvironmentPresentation, "connection" | "serverCo

export function createScratchProjectActions(input: {
readonly environments: () => Iterable<ScratchEnvironment>;
readonly primaryEnvironmentId: () => EnvironmentId | null;
readonly registry: AtomRegistry.AtomRegistry;
readonly handleNewThread: NewThreadHandler;
}) {
Expand All @@ -43,8 +48,11 @@ export function createScratchProjectActions(input: {
return availableScratchWorkspaceRoot(environment?.connection.phase, environment?.serverConfig);
};

const scratchEnvironmentId = (current: EnvironmentId | null): EnvironmentId | null => {
if (current !== null) return scratchWorkspaceRootFor(current) !== null ? current : null;
// The primary (local) environment wins; `fallback` only applies when it offers no scratch root.
const scratchEnvironmentId = (fallback: EnvironmentId | null): EnvironmentId | null => {
const primary = input.primaryEnvironmentId();
if (primary !== null && scratchWorkspaceRootFor(primary) !== null) return primary;
if (fallback !== null && scratchWorkspaceRootFor(fallback) !== null) return fallback;
const offering = [...input.environments()].filter(
(entry) => scratchWorkspaceRootFor(entry.environmentId) !== null,
);
Expand Down Expand Up @@ -85,10 +93,12 @@ export function createScratchProjectActions(input: {

export function useScratchProject() {
const { environments } = useEnvironments();
const primaryEnvironmentId = usePrimaryEnvironmentId();
const registry = useContext(RegistryContext);
const handleNewThread = useNewThreadHandler();
return createScratchProjectActions({
environments: () => environments,
primaryEnvironmentId: () => primaryEnvironmentId,
registry,
handleNewThread,
});
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/shortcuts/chatRouteCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export function installChatRouteCommands(router: AnyRouter): () => void {
[...appAtomRegistry.get(environmentPresentations.presentationsAtom)].map(
([environmentId, presentation]) => ({ ...presentation, environmentId }),
),
primaryEnvironmentId: () => appAtomRegistry.get(primaryEnvironmentIdAtom),
registry: appAtomRegistry,
handleNewThread,
});
Expand Down Expand Up @@ -109,9 +110,7 @@ export function installChatRouteCommands(router: AnyRouter): () => void {
"chat.newWithoutProject": (event) => {
const { activeThread, activeDraftThread } = readThreadContext();
const environmentId = scratch.scratchEnvironmentId(
activeThread?.environmentId ??
activeDraftThread?.environmentId ??
appAtomRegistry.get(primaryEnvironmentIdAtom),
activeThread?.environmentId ?? activeDraftThread?.environmentId ?? null,
);
if (environmentId === null) return;
claim(event);
Expand Down
Loading