authorgravatar for 71498452+StiensWout@users.noreply.github.comWout Stiens <71498452+StiensWout@users.noreply.github.com> 2026-07-30 04:14:06+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-29 22:14:06-04:00
log758decaa565f021f85af756669b18bd9f80cacf5
tree2e4d4a9bcf0e615ced31e20be6bbe2bad7b3a6cd
parent2d9066e089c241c3befd7a4632efe493fabd70d9
signature Signed by PGP key B5690EEEBB952194

fix(clients): disable add project while disconnected (#4834)


7 files changed, 243 insertions(+), 56 deletions(-)

apps/mobile/src/features/projects/AddProjectScreen.logic.test.ts created+41
......@@ -0,0 +1,41 @@
1import type { EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection";
2import { EnvironmentId } from "@t3tools/contracts";
3import { describe, expect, it } from "vite-plus/test";
4
5import { resolveAddProjectEnvironment } from "./AddProjectScreen.logic";
6
7const ENVIRONMENT_A = EnvironmentId.make("environment-a");
8const ENVIRONMENT_B = EnvironmentId.make("environment-b");
9
10function environment(environmentId: EnvironmentId, connectionState: EnvironmentConnectionPhase) {
11 return { environmentId, connectionState };
12}
13
14describe("resolveAddProjectEnvironment", () => {
15 it("does not redirect an explicit unavailable environment to another environment", () => {
16 expect(
17 resolveAddProjectEnvironment(
18 [environment(ENVIRONMENT_A, "offline"), environment(ENVIRONMENT_B, "connected")],
19 ENVIRONMENT_A,
20 ),
21 ).toBeNull();
22 });
23
24 it("resolves an explicit connected environment", () => {
25 expect(
26 resolveAddProjectEnvironment(
27 [environment(ENVIRONMENT_A, "connected"), environment(ENVIRONMENT_B, "connected")],
28 ENVIRONMENT_A,
29 )?.environmentId,
30 ).toBe(ENVIRONMENT_A);
31 });
32
33 it("defaults to the first connected environment when no environment is requested", () => {
34 expect(
35 resolveAddProjectEnvironment(
36 [environment(ENVIRONMENT_A, "offline"), environment(ENVIRONMENT_B, "connected")],
37 null,
38 )?.environmentId,
39 ).toBe(ENVIRONMENT_B);
40 });
41});
apps/mobile/src/features/projects/AddProjectScreen.logic.ts created+26
......@@ -0,0 +1,26 @@
1import { canCreateProjectInEnvironment } from "@t3tools/client-runtime/operations/projects";
2import type { EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection";
3import type { EnvironmentId } from "@t3tools/contracts";
4
5export function resolveAddProjectEnvironment<
6 T extends {
7 readonly environmentId: EnvironmentId;
8 readonly connectionState: EnvironmentConnectionPhase;
9 },
10>(environmentOptions: ReadonlyArray<T>, requestedEnvironmentId: EnvironmentId | null): T | null {
11 if (requestedEnvironmentId !== null) {
12 return (
13 environmentOptions.find(
14 (environment) =>
15 environment.environmentId === requestedEnvironmentId &&
16 canCreateProjectInEnvironment(environment.connectionState),
17 ) ?? null
18 );
19 }
20
21 return (
22 environmentOptions.find((environment) =>
23 canCreateProjectInEnvironment(environment.connectionState),
24 ) ?? null
25 );
26}
apps/mobile/src/features/projects/AddProjectScreen.tsx+79-40
......@@ -4,12 +4,17 @@ import {
44 addProjectRemoteSourceProvider,
55 buildAddProjectRemoteSourceReadiness,
66 buildProjectCreateCommand,
7 canCreateProjectInEnvironment,
78 findExistingAddProject,
89 getAddProjectInitialQuery,
910 resolveAddProjectPath,
1011 sortAddProjectProviderSources,
1112 type AddProjectRemoteSource,
1213} from "@t3tools/client-runtime/operations/projects";
14import {
15 connectionStatusText,
16 type EnvironmentConnectionPhase,
17} from "@t3tools/client-runtime/connection";
1318import {
1419 canPreloadBrowsePath,
1520 createBrowseNavigationCoordinator,
......@@ -24,7 +29,7 @@ import {
2429import { CommandId, type EnvironmentId, ProjectId } from "@t3tools/contracts";
2530import { StackActions, useNavigation } from "@react-navigation/native";
2631import { SymbolView } from "../../components/AppSymbol";
27import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
32import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
2833import { ActivityIndicator, Alert, Pressable, ScrollView, View } from "react-native";
2934import { useSafeAreaInsets } from "react-native-safe-area-context";
3035import * as Arr from "effect/Array";
......@@ -46,15 +51,20 @@ import { uuidv4 } from "../../lib/uuid";
4651import { useAtomCommand } from "../../state/use-atom-command";
4752import { useAtomQueryRunner } from "../../state/use-atom-query-runner";
4853import {
54 useRemoteConnectionStatus,
4955 useRemoteEnvironmentRuntime,
5056 useSavedRemoteConnections,
5157} from "../../state/use-remote-environment-registry";
58import { resolveAddProjectEnvironment } from "./AddProjectScreen.logic";
5259
5360interface EnvironmentOption {
5461 readonly environmentId: EnvironmentId;
5562 readonly label: string;
5663 readonly platform: string;
5764 readonly baseDirectory: string | null;
65 readonly connectionState: EnvironmentConnectionPhase;
66 readonly connectionError: string | null;
67 readonly connectionErrorTraceId: string | null;
5868}
5969
6070const environmentOptionOrder = Order.mapInput(
......@@ -227,10 +237,13 @@ function ProjectPathInput(props: {
227237}
228238
229239function useBrowsePathInput(environment: EnvironmentOption | null) {
240 const environmentId = environment?.environmentId ?? null;
241 const environmentBaseDirectory = environment?.baseDirectory ?? null;
230242 const [pathInput, commitPathInput] = useState(() =>
231 getAddProjectInitialQuery(environment?.baseDirectory),
243 getAddProjectInitialQuery(environmentBaseDirectory),
232244 );
233 const environmentRuntime = useRemoteEnvironmentRuntime(environment?.environmentId ?? null);
245 const previousEnvironmentIdRef = useRef(environmentId);
246 const environmentRuntime = useRemoteEnvironmentRuntime(environmentId);
234247 const loadBrowsePath = useAtomQueryRunner(filesystemEnvironment.browse, {
235248 reportFailure: false,
236249 reportDefect: false,
......@@ -268,10 +281,11 @@ function useBrowsePathInput(environment: EnvironmentOption | null) {
268281 );
269282
270283 useEffect(() => {
271 if (environment) {
272 setPathInput(getAddProjectInitialQuery(environment.baseDirectory));
284 if (environmentId !== null && environmentId !== previousEnvironmentIdRef.current) {
285 previousEnvironmentIdRef.current = environmentId;
286 setPathInput(getAddProjectInitialQuery(environmentBaseDirectory));
273287 }
274 }, [environment, setPathInput]);
288 }, [environmentBaseDirectory, environmentId, setPathInput]);
275289
276290 useEffect(
277291 () => () => {
......@@ -286,19 +300,27 @@ function useBrowsePathInput(environment: EnvironmentOption | null) {
286300function useEnvironmentOptions(): ReadonlyArray<EnvironmentOption> {
287301 const serverConfigByEnvironmentId = useServerConfigs();
288302 const { savedConnectionsById } = useSavedRemoteConnections();
303 const { connectedEnvironments } = useRemoteConnectionStatus();
289304
290305 return useMemo<ReadonlyArray<EnvironmentOption>>(() => {
306 const runtimeByEnvironmentId = new Map(
307 connectedEnvironments.map((environment) => [environment.environmentId, environment] as const),
308 );
291309 const options = Object.values(savedConnectionsById).map((connection) => {
292310 const config = serverConfigByEnvironmentId.get(connection.environmentId);
311 const runtime = runtimeByEnvironmentId.get(connection.environmentId);
293312 return {
294313 environmentId: connection.environmentId,
295314 label: connection.environmentLabel,
296315 platform: platformFromOs(config?.environment.platform.os ?? null),
297316 baseDirectory: config?.settings.addProjectBaseDirectory ?? null,
317 connectionState: runtime?.connectionState ?? "available",
318 connectionError: runtime?.connectionError ?? null,
319 connectionErrorTraceId: runtime?.connectionErrorTraceId ?? null,
298320 };
299321 });
300322 return Arr.sort(options, environmentOptionOrder);
301 }, [savedConnectionsById, serverConfigByEnvironmentId]);
323 }, [connectedEnvironments, savedConnectionsById, serverConfigByEnvironmentId]);
302324}
303325
304326function useSelectedEnvironment(): {
......@@ -309,8 +331,14 @@ function useSelectedEnvironment(): {
309331 const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<EnvironmentId | null>(null);
310332 const environmentOptions = useEnvironmentOptions();
311333 const selectedEnvironment =
312 environmentOptions.find((environment) => environment.environmentId === selectedEnvironmentId) ??
313 environmentOptions[0] ??
334 environmentOptions.find(
335 (environment) =>
336 environment.environmentId === selectedEnvironmentId &&
337 canCreateProjectInEnvironment(environment.connectionState),
338 ) ??
339 environmentOptions.find((environment) =>
340 canCreateProjectInEnvironment(environment.connectionState),
341 ) ??
314342 null;
315343
316344 return {
......@@ -325,9 +353,9 @@ function EmptyEnvironmentState() {
325353
326354 return (
327355 <View className="items-center gap-3 rounded-2xl bg-card px-5 py-8">
328 <Text className="text-center text-lg font-t3-bold">No environments connected</Text>
356 <Text className="text-center text-lg font-t3-bold">Environment unavailable</Text>
329357 <Text className="text-center text-sm leading-normal text-foreground-muted">
330 Add an environment before adding a project.
358 Start or reconnect an environment before adding a project.
331359 </Text>
332360 <Pressable
333361 onPress={() => navigation.dispatch(StackActions.replace("ConnectionsNew"))}
......@@ -407,17 +435,25 @@ export function AddProjectSourceScreen() {
407435
408436 return (
409437 <AddProjectShell>
410 {environmentOptions.length === 0 ? <EmptyEnvironmentState /> : null}
438 {selectedEnvironment === null ? <EmptyEnvironmentState /> : null}
411439
412440 {environmentOptions.length > 1 ? (
413441 <>
414 <SectionTitle>Connected environments</SectionTitle>
442 <SectionTitle>Environments</SectionTitle>
415443 <ListSection>
416444 {environmentOptions.map((environment, index) => (
417445 <ListRow
418446 key={environment.environmentId}
419447 title={environment.label}
420 subtitle={environment.environmentId}
448 subtitle={
449 canCreateProjectInEnvironment(environment.connectionState)
450 ? environment.environmentId
451 : connectionStatusText({
452 phase: environment.connectionState,
453 error: environment.connectionError,
454 traceId: environment.connectionErrorTraceId,
455 })
456 }
421457 icon={
422458 <SymbolView
423459 name="server.rack"
......@@ -427,6 +463,7 @@ export function AddProjectSourceScreen() {
427463 />
428464 }
429465 selected={environment.environmentId === selectedEnvironment?.environmentId}
466 disabled={!canCreateProjectInEnvironment(environment.connectionState)}
430467 isFirst={index === 0}
431468 right={
432469 environment.environmentId === selectedEnvironment?.environmentId ? (
......@@ -500,7 +537,7 @@ function useCreateProject(environment: EnvironmentOption | null) {
500537
501538 return useCallback(
502539 async (workspaceRoot: string) => {
503 if (!environment) return;
540 if (!environment || !canCreateProjectInEnvironment(environment.connectionState)) return;
504541
505542 const existing = findExistingAddProject({
506543 projects,
......@@ -551,11 +588,7 @@ function useEnvironmentFromParam(
551588): EnvironmentOption | null {
552589 const environmentOptions = useEnvironmentOptions();
553590 const environmentId = stringParam(environmentIdParam) as EnvironmentId | null;
554 return (
555 environmentOptions.find((environment) => environment.environmentId === environmentId) ??
556 environmentOptions[0] ??
557 null
558 );
591 return resolveAddProjectEnvironment(environmentOptions, environmentId);
559592}
560593
561594export function AddProjectRepositoryScreen(props: {
......@@ -619,26 +652,32 @@ export function AddProjectRepositoryScreen(props: {
619652 return (
620653 <AddProjectShell>
621654 {error ? <ErrorBanner message={error} /> : null}
622 <TextInput
623 className="h-12 min-h-12 rounded-[24px] px-4 py-0 text-base leading-snug"
624 value={repositoryInput}
625 onChangeText={setRepositoryInput}
626 autoCapitalize="none"
627 autoCorrect={false}
628 placeholder={
629 source === "url"
630 ? "https://github.com/org/repo.git"
631 : addProjectRemoteSourcePathHint(source)
632 }
633 returnKeyType="next"
634 onSubmitEditing={() => void lookupRepository()}
635 />
636 <PrimaryActionButton
637 label={source === "url" ? "Continue" : "Lookup repository"}
638 disabled={isSubmitting || repositoryInput.trim().length === 0}
639 onPress={() => void lookupRepository()}
640 loading={isSubmitting}
641 />
655 {environment ? (
656 <>
657 <TextInput
658 className="h-12 min-h-12 rounded-[24px] px-4 py-0 text-base leading-snug"
659 value={repositoryInput}
660 onChangeText={setRepositoryInput}
661 autoCapitalize="none"
662 autoCorrect={false}
663 placeholder={
664 source === "url"
665 ? "https://github.com/org/repo.git"
666 : addProjectRemoteSourcePathHint(source)
667 }
668 returnKeyType="next"
669 onSubmitEditing={() => void lookupRepository()}
670 />
671 <PrimaryActionButton
672 label={source === "url" ? "Continue" : "Lookup repository"}
673 disabled={isSubmitting || repositoryInput.trim().length === 0}
674 onPress={() => void lookupRepository()}
675 loading={isSubmitting}
676 />
677 </>
678 ) : (
679 <EmptyEnvironmentState />
680 )}
642681 </AddProjectShell>
643682 );
644683}
apps/mobile/src/features/threads/NewTaskRouteScreen.tsx+18-12
......@@ -203,13 +203,17 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara
203203 title={screenTitle}
204204 subtitle={incomingShareSubtitle}
205205 onBack={layout.usesSplitView ? () => navigation.goBack() : undefined}
206 actions={[
207 {
208 accessibilityLabel: "Add project",
209 icon: "plus",
210 onPress: () => navigation.navigate("NewTaskSheet", { screen: "AddProject" }),
211 },
212 ]}
206 actions={
207 catalogState.hasReadyEnvironment
208 ? [
209 {
210 accessibilityLabel: "Add project",
211 icon: "plus",
212 onPress: () => navigation.navigate("NewTaskSheet", { screen: "AddProject" }),
213 },
214 ]
215 : []
216 }
213217 />
214218 </>
215219 ) : (
......@@ -229,11 +233,13 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara
229233 separateBackground
230234 />
231235 ) : null}
232 <NativeHeaderToolbar.Button
233 icon="plus"
234 onPress={() => navigation.navigate("NewTaskSheet", { screen: "AddProject" })}
235 separateBackground
236 />
236 {catalogState.hasReadyEnvironment ? (
237 <NativeHeaderToolbar.Button
238 icon="plus"
239 onPress={() => navigation.navigate("NewTaskSheet", { screen: "AddProject" })}
240 separateBackground
241 />
242 ) : null}
237243 </NativeHeaderToolbar>
238244 </>
239245 )}
apps/web/src/components/CommandPalette.tsx+62-4
......@@ -1,6 +1,8 @@
11"use client";
22
33import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment";
4import { canCreateProjectInEnvironment } from "@t3tools/client-runtime/operations/projects";
5import { connectionStatusText } from "@t3tools/client-runtime/connection";
46import {
57 canPreloadBrowsePath,
68 createBrowseNavigationCoordinator,
......@@ -163,6 +165,8 @@ interface AddProjectEnvironmentOption {
163165 readonly environmentId: EnvironmentId;
164166 readonly label: string;
165167 readonly isPrimary: boolean;
168 readonly isConnected: boolean;
169 readonly status: string;
166170}
167171
168172type AddProjectRemoteProviderKind = Extract<
......@@ -620,6 +624,8 @@ function OpenCommandPaletteDialog(props: {
620624 runtimeLabel: environment.label,
621625 }),
622626 isPrimary,
627 isConnected: canCreateProjectInEnvironment(environment.connection.phase),
628 status: connectionStatusText(environment.connection),
623629 };
624630 });
625631
......@@ -632,10 +638,14 @@ function OpenCommandPaletteDialog(props: {
632638
633639 return options;
634640 }, [environments]);
635 const defaultAddProjectEnvironmentId = addProjectEnvironmentOptions[0]?.environmentId ?? null;
641 const defaultAddProjectEnvironmentId =
642 addProjectEnvironmentOptions.find((option) => option.isConnected)?.environmentId ?? null;
636643 const wslAddProjectEnvironmentOption = useMemo(
637644 () =>
638645 addProjectEnvironmentOptions.find((option) => {
646 if (!option.isConnected) {
647 return false;
648 }
639649 const environment = environments.find(
640650 (candidate) => candidate.environmentId === option.environmentId,
641651 );
......@@ -1108,6 +1118,19 @@ function OpenCommandPaletteDialog(props: {
11081118
11091119 const startAddProjectSourceSelection = useCallback(
11101120 (environmentId: EnvironmentId): void => {
1121 const environment = environments.find(
1122 (candidate) => candidate.environmentId === environmentId,
1123 );
1124 if (!canCreateProjectInEnvironment(environment?.connection.phase)) {
1125 toastManager.add(
1126 stackedThreadToast({
1127 type: "error",
1128 title: "Environment unavailable",
1129 description: `${environment?.label ?? "The selected environment"} is not connected.`,
1130 }),
1131 );
1132 return;
1133 }
11111134 setAddProjectEnvironmentId(environmentId);
11121135 setAddProjectCloneFlow(null);
11131136 pushPaletteView({
......@@ -1123,6 +1146,7 @@ function OpenCommandPaletteDialog(props: {
11231146 [
11241147 browseEnvironmentId,
11251148 buildAddProjectSourceGroups,
1149 environments,
11261150 pushPaletteView,
11271151 sourceControlDiscovery.data,
11281152 ],
......@@ -1134,7 +1158,12 @@ function OpenCommandPaletteDialog(props: {
11341158 value: `action:add-project:environment:${option.environmentId}`,
11351159 searchTerms: [option.label, option.environmentId, option.isPrimary ? "this device" : ""],
11361160 title: option.label,
1137 description: option.isPrimary ? "This device" : option.environmentId,
1161 description: option.isConnected
1162 ? option.isPrimary
1163 ? "This device"
1164 : option.environmentId
1165 : option.status,
1166 disabled: !option.isConnected,
11381167 icon: <FolderPlusIcon className={ITEM_ICON_CLASS} />,
11391168 keepOpen: true,
11401169 run: async () => {
......@@ -1155,7 +1184,7 @@ function OpenCommandPaletteDialog(props: {
11551184 );
11561185
11571186 const openAddProjectFlow = useCallback(() => {
1158 if (addProjectEnvironmentOptions.length > 1) {
1187 if (addProjectEnvironmentOptions.length > 1 || defaultAddProjectEnvironmentId === null) {
11591188 pushPaletteView({
11601189 addonIcon: <FolderPlusIcon className={ADDON_ICON_CLASS} />,
11611190 groups: addProjectEnvironmentGroups,
......@@ -1294,6 +1323,7 @@ function OpenCommandPaletteDialog(props: {
12941323 "environment",
12951324 ],
12961325 title: "Add project",
1326 disabled: defaultAddProjectEnvironmentId === null,
12971327 icon: <FolderPlusIcon className={ITEM_ICON_CLASS} />,
12981328 keepOpen: true,
12991329 run: async () => {
......@@ -1355,6 +1385,19 @@ function OpenCommandPaletteDialog(props: {
13551385 readonly platform: string;
13561386 readonly currentProjectCwd: string | null;
13571387 }) => {
1388 const environment = environments.find(
1389 (candidate) => candidate.environmentId === input.environmentId,
1390 );
1391 if (!canCreateProjectInEnvironment(environment?.connection.phase)) {
1392 toastManager.add(
1393 stackedThreadToast({
1394 type: "error",
1395 title: "Environment unavailable",
1396 description: `${environment?.label ?? "The selected environment"} is not connected.`,
1397 }),
1398 );
1399 return;
1400 }
13581401 const rawCwd = input.rawCwd;
13591402
13601403 if (isUnsupportedWindowsProjectPath(rawCwd.trim(), input.platform)) {
......@@ -1507,6 +1550,16 @@ function OpenCommandPaletteDialog(props: {
15071550 if (!addProjectCloneFlow) {
15081551 return;
15091552 }
1553 if (!canCreateProjectInEnvironment(browseEnvironment?.connection.phase)) {
1554 toastManager.add(
1555 stackedThreadToast({
1556 type: "error",
1557 title: "Environment unavailable",
1558 description: `${browseEnvironment?.label ?? "The selected environment"} is not connected.`,
1559 }),
1560 );
1561 return;
1562 }
15101563
15111564 if (addProjectCloneFlow.step === "repository") {
15121565 const rawRepository = query.trim();
......@@ -1711,7 +1764,10 @@ function OpenCommandPaletteDialog(props: {
17111764 getCommandPaletteInputPlaceholder(paletteMode);
17121765 const isSubmenu = paletteMode === "submenu" || paletteMode === "submenu-browse";
17131766 const hasHighlightedBrowseItem = highlightedItemValue?.startsWith("browse:") ?? false;
1714 const canSubmitBrowsePath = isBrowsing && !relativePathNeedsActiveProject;
1767 const canSubmitBrowsePath =
1768 isBrowsing &&
1769 !relativePathNeedsActiveProject &&
1770 canCreateProjectInEnvironment(browseEnvironment?.connection.phase);
17151771 const willCreateProjectPath =
17161772 canSubmitBrowsePath &&
17171773 !isBrowsePending &&
......@@ -1738,6 +1794,7 @@ function OpenCommandPaletteDialog(props: {
17381794 const canSubmitRemoteProjectFlow =
17391795 addProjectCloneFlow?.step === "repository" &&
17401796 query.trim().length > 0 &&
1797 canCreateProjectInEnvironment(browseEnvironment?.connection.phase) &&
17411798 !isRemoteProjectPending;
17421799 const fileManagerName = getLocalFileManagerName(navigator.platform);
17431800 const canOpenProjectFromFileManager =
......@@ -2063,6 +2120,7 @@ function OpenCommandPaletteDialog(props: {
20632120 )}
20642121 aria-label={`${submitActionLabel} (${addShortcutLabel})`}
20652122 disabled={
2123 !canCreateProjectInEnvironment(browseEnvironment?.connection.phase) ||
20662124 relativePathNeedsActiveProject ||
20672125 (isCloneDestinationStep && isRemoteProjectPending)
20682126 }
packages/client-runtime/src/operations/projects.test.ts+10
......@@ -10,6 +10,7 @@ import * as Option from "effect/Option";
1010import {
1111 buildAddProjectRemoteSourceReadiness,
1212 buildProjectCreateCommand,
13 canCreateProjectInEnvironment,
1314 findExistingAddProject,
1415 getAddProjectInitialQuery,
1516 resolveAddProjectPath,
......@@ -18,6 +19,15 @@ import {
1819import type { EnvironmentProject } from "../state/models.ts";
1920
2021describe("add project shared logic", () => {
22 it("only allows project creation in connected environments", () => {
23 expect(canCreateProjectInEnvironment("connected")).toBe(true);
24 expect(canCreateProjectInEnvironment("available")).toBe(false);
25 expect(canCreateProjectInEnvironment("offline")).toBe(false);
26 expect(canCreateProjectInEnvironment("connecting")).toBe(false);
27 expect(canCreateProjectInEnvironment("reconnecting")).toBe(false);
28 expect(canCreateProjectInEnvironment("error")).toBe(false);
29 });
30
2131 it("resolves initial browse paths from settings", () => {
2232 expect(getAddProjectInitialQuery("")).toBe("~/");
2333 expect(getAddProjectInitialQuery("/work")).toBe("/work/");
packages/client-runtime/src/operations/projects.ts+7
......@@ -1,3 +1,4 @@
1import type { EnvironmentConnectionPhase } from "../connection/presentation.ts";
12import type {
23 CommandId,
34 EnvironmentId,
......@@ -27,6 +28,12 @@ export type AddProjectRemoteProviderKind = Extract<
2728>;
2829export type AddProjectRemoteSource = AddProjectRemoteProviderKind | "url";
2930
31export function canCreateProjectInEnvironment(
32 connectionPhase: EnvironmentConnectionPhase | null | undefined,
33): boolean {
34 return connectionPhase === "connected";
35}
36
3037export type AddProjectRemoteSourceReadiness = Record<
3138 AddProjectRemoteSource,
3239 { readonly ready: boolean; readonly hint: string | null }