Skip to content

Commit 98e2c80

Browse files
committed
gate the cloud default on settled signals
1 parent 0f530d5 commit 98e2c80

7 files changed

Lines changed: 154 additions & 21 deletions

File tree

packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
useState,
99
} from "react";
1010
import { useConnectivity } from "../../../hooks/useConnectivity";
11+
import { useUserRepositoryIntegration } from "../../integrations/useIntegrations";
1112
import { PromptInput } from "../../message-editor/components/PromptInput";
1213
import { useDraftStore } from "../../message-editor/draftStore";
1314
import type { EditorHandle } from "../../message-editor/types";
@@ -22,8 +23,10 @@ import {
2223
type WorkspaceMode,
2324
WorkspaceModeSelect,
2425
} from "../../task-detail/components/WorkspaceModeSelect";
26+
import { useCloudModeEnabled } from "../../task-detail/hooks/useCloudModeEnabled";
2527
import { usePreviewConfig } from "../../task-detail/hooks/usePreviewConfig";
2628
import { useTaskCreation } from "../../task-detail/hooks/useTaskCreation";
29+
import { resolveWorkspaceModePreference } from "../../task-detail/hooks/workspaceModePreference";
2730

2831
export interface ChannelHomeComposerHandle {
2932
/** Drop a starter prompt into the editor and apply its mode, if any. */
@@ -77,10 +80,18 @@ export const ChannelHomeComposer = forwardRef<
7780
[setLastUsedAdapter],
7881
);
7982

83+
const cloudModeEnabled = useCloudModeEnabled();
84+
const { hasGithubIntegration } = useUserRepositoryIntegration();
85+
8086
// Repo-less channel tasks only run local or cloud (worktree needs a repo), so
8187
// collapse any lingering worktree preference down to local for the initial pick.
82-
const [workspaceMode, setWorkspaceModeState] = useState<WorkspaceMode>(
83-
lastUsedWorkspaceMode === "cloud" ? "cloud" : "local",
88+
const [workspaceMode, setWorkspaceModeState] = useState<WorkspaceMode>(() =>
89+
resolveWorkspaceModePreference({
90+
preferredMode: lastUsedWorkspaceMode === "cloud" ? "cloud" : "local",
91+
cloudModeEnabled,
92+
hasGithubIntegration,
93+
lastUsedLocalWorkspaceMode: "local",
94+
}),
8495
);
8596
const [selectedCloudEnvId, setSelectedCloudEnvId] = useState<string | null>(
8697
null,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { useService } from "@posthog/di/react";
2+
import { useEffect, useState } from "react";
3+
import { FEATURE_FLAGS, type FeatureFlags } from "./identifiers";
4+
5+
export function useFeatureFlagsLoaded(): boolean {
6+
const flags = useService<FeatureFlags>(FEATURE_FLAGS);
7+
const [loaded, setLoaded] = useState(false);
8+
9+
useEffect(() => flags.onFlagsLoaded(() => setLoaded(true)), [flags]);
10+
11+
return loaded;
12+
}

packages/ui/src/features/settings/settingsStore.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
11
import { registerRendererStateStorage } from "@posthog/ui/shell/rendererStorage";
22
import { beforeEach, describe, expect, it, vi } from "vitest";
3-
import { type CompletionSound, useSettingsStore } from "./settingsStore";
3+
import {
4+
type CompletionSound,
5+
DEFAULT_WORKSPACE_MODE,
6+
useSettingsStore,
7+
} from "./settingsStore";
48

59
const getItem = vi.fn();
610
const setItem = vi.fn();
711
const removeItem = vi.fn();
812

913
registerRendererStateStorage({ getItem, setItem, removeItem });
1014

15+
// Runs before any test mutates the store singleton, so getState() still
16+
// reflects the initial values.
17+
describe("feature settingsStore defaults", () => {
18+
it("defaults the workspace mode to cloud with a local fallback", () => {
19+
expect(DEFAULT_WORKSPACE_MODE).toBe("cloud");
20+
expect(useSettingsStore.getState().lastUsedWorkspaceMode).toBe("cloud");
21+
expect(useSettingsStore.getState().lastUsedLocalWorkspaceMode).toBe(
22+
"local",
23+
);
24+
});
25+
});
26+
1127
describe("feature settingsStore cloud selections", () => {
1228
beforeEach(() => {
1329
getItem.mockReset();

packages/ui/src/features/settings/settingsStore.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { persist } from "zustand/middleware";
1212

1313
export type DefaultRunMode = "local" | "cloud" | "last_used";
1414
export type LocalWorkspaceMode = "worktree" | "local";
15+
16+
export const DEFAULT_WORKSPACE_MODE: WorkspaceMode = "cloud";
1517
export type AgentAdapter = Adapter;
1618
export type DefaultInitialTaskMode = "plan" | "last_used";
1719
export type DefaultMessagingMode = "queue" | "steer";
@@ -221,7 +223,7 @@ export const useSettingsStore = create<SettingsStore>()(
221223
defaultRunMode: "last_used",
222224
lastUsedRunMode: "local",
223225
lastUsedLocalWorkspaceMode: "local",
224-
lastUsedWorkspaceMode: "cloud",
226+
lastUsedWorkspaceMode: DEFAULT_WORKSPACE_MODE,
225227
lastUsedAdapter: "claude",
226228
lastUsedModel: null,
227229
lastUsedReasoningEffort: null,

packages/ui/src/features/task-detail/components/TaskInput.tsx

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { useAutoresearchEnabled } from "../../autoresearch/useAutoresearchEnable
3737
import { useFileSearchStore } from "../../command/fileSearchStore";
3838
import { NewTaskFilePreview } from "../../command/NewTaskFilePreview";
3939
import { EnvironmentSelector } from "../../environments/EnvironmentSelector";
40+
import { useFeatureFlagsLoaded } from "../../feature-flags/useFeatureFlagsLoaded";
4041
import { AdditionalDirectoriesButton } from "../../folder-picker/AdditionalDirectoriesButton";
4142
import { FolderPicker } from "../../folder-picker/FolderPicker";
4243
import { GitHubRepoPicker } from "../../folder-picker/GitHubRepoPicker";
@@ -72,6 +73,7 @@ import { UnifiedModelSelector } from "../../sessions/components/UnifiedModelSele
7273
import { getCurrentModeFromConfigOptions } from "../../sessions/sessionStore";
7374
import {
7475
type AgentAdapter,
76+
DEFAULT_WORKSPACE_MODE,
7577
useSettingsStore,
7678
} from "../../settings/settingsStore";
7779
import { useSkills } from "../../skills/useSkills";
@@ -83,6 +85,7 @@ import {
8385
import { usePreviewConfig } from "../hooks/usePreviewConfig";
8486
import { useTaskCreation } from "../hooks/useTaskCreation";
8587
import { useWarmTask } from "../hooks/useWarmTask";
88+
import { resolveWorkspaceModePreference } from "../hooks/workspaceModePreference";
8689
import { CloudGithubMissingNotice } from "./CloudGithubMissingNotice";
8790
import { NewTaskSuggestions } from "./ContinueCliSessions";
8891
import {
@@ -315,43 +318,57 @@ export function TaskInput({
315318
} = useUserRepositoryIntegration();
316319

317320
const cloudModeEnabled = useCloudModeEnabled();
321+
const flagsLoaded = useFeatureFlagsLoaded();
318322
const reposReady = areReposReady({
319323
isLoadingRepos,
320324
repositoriesCount: repositories.length,
321325
hasGithubIntegration,
322326
});
323327

324-
// Cloud is the default, but only when it works out of the box: with the flag
325-
// off or GitHub not connected, fall back to the last local mode instead of
326-
// stranding the user behind a connect-GitHub prompt.
327-
const resolveWorkspaceModePreference = useCallback(
328-
(mode: WorkspaceMode): WorkspaceMode =>
329-
mode === "cloud" && (!cloudModeEnabled || !hasGithubIntegration)
330-
? lastUsedLocalWorkspaceMode
331-
: mode,
332-
[cloudModeEnabled, hasGithubIntegration, lastUsedLocalWorkspaceMode],
333-
);
334-
335328
const [workspaceMode, setWorkspaceModeState] = useState<WorkspaceMode>(() => {
336329
if (initialCloudRepository) return "cloud";
337-
return resolveWorkspaceModePreference(lastUsedWorkspaceMode || "cloud");
330+
return resolveWorkspaceModePreference({
331+
preferredMode: lastUsedWorkspaceMode || DEFAULT_WORKSPACE_MODE,
332+
cloudModeEnabled,
333+
hasGithubIntegration,
334+
lastUsedLocalWorkspaceMode,
335+
});
338336
});
339337

338+
// A positive flag or integration signal is final, but a negative one may
339+
// just mean the async flag fetch or integrations query hasn't landed yet, so
340+
// a cloud preference only resolves once each negative signal is settled.
341+
const cloudSignalsSettled =
342+
(cloudModeEnabled || flagsLoaded) &&
343+
(hasGithubIntegration || !isLoadingRepos);
344+
340345
const didResolveWorkspaceModeRef = useRef(false);
341346
useEffect(() => {
342347
if (didResolveWorkspaceModeRef.current) return;
343-
if (!settingsHydrated || !reposReady) return;
348+
if (!settingsHydrated) return;
349+
if (initialCloudRepository) {
350+
didResolveWorkspaceModeRef.current = true;
351+
return;
352+
}
353+
const preferredMode = lastUsedWorkspaceMode || DEFAULT_WORKSPACE_MODE;
354+
if (preferredMode === "cloud" && !cloudSignalsSettled) return;
344355
didResolveWorkspaceModeRef.current = true;
345-
if (initialCloudRepository) return;
346356
setWorkspaceModeState(
347-
resolveWorkspaceModePreference(lastUsedWorkspaceMode || "cloud"),
357+
resolveWorkspaceModePreference({
358+
preferredMode,
359+
cloudModeEnabled,
360+
hasGithubIntegration,
361+
lastUsedLocalWorkspaceMode,
362+
}),
348363
);
349364
}, [
350365
settingsHydrated,
351-
reposReady,
352366
lastUsedWorkspaceMode,
353367
initialCloudRepository,
354-
resolveWorkspaceModePreference,
368+
cloudSignalsSettled,
369+
cloudModeEnabled,
370+
hasGithubIntegration,
371+
lastUsedLocalWorkspaceMode,
355372
]);
356373

357374
const setWorkspaceMode = (mode: WorkspaceMode) => {
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { describe, expect, it } from "vitest";
2+
import { resolveWorkspaceModePreference } from "./workspaceModePreference";
3+
4+
describe("resolveWorkspaceModePreference", () => {
5+
it.each([
6+
["cloud", true, true, "local", "cloud"],
7+
["cloud", false, true, "local", "local"],
8+
["cloud", true, false, "local", "local"],
9+
["cloud", false, false, "local", "local"],
10+
["cloud", false, true, "worktree", "worktree"],
11+
["cloud", true, false, "worktree", "worktree"],
12+
] as const)(
13+
"resolves %s (flag %s, integration %s, local fallback %s) to %s",
14+
(
15+
preferredMode,
16+
cloudModeEnabled,
17+
hasGithubIntegration,
18+
lastUsedLocalWorkspaceMode,
19+
expected,
20+
) => {
21+
expect(
22+
resolveWorkspaceModePreference({
23+
preferredMode,
24+
cloudModeEnabled,
25+
hasGithubIntegration,
26+
lastUsedLocalWorkspaceMode,
27+
}),
28+
).toBe(expected);
29+
},
30+
);
31+
32+
it.each([
33+
["local", true, true],
34+
["local", false, false],
35+
["worktree", true, false],
36+
["worktree", false, true],
37+
] as const)(
38+
"passes %s through untouched (flag %s, integration %s)",
39+
(preferredMode, cloudModeEnabled, hasGithubIntegration) => {
40+
expect(
41+
resolveWorkspaceModePreference({
42+
preferredMode,
43+
cloudModeEnabled,
44+
hasGithubIntegration,
45+
lastUsedLocalWorkspaceMode: "worktree",
46+
}),
47+
).toBe(preferredMode);
48+
},
49+
);
50+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { WorkspaceMode } from "@posthog/shared";
2+
import type { LocalWorkspaceMode } from "../../settings/settingsStore";
3+
4+
export interface WorkspaceModePreferenceInput {
5+
preferredMode: WorkspaceMode;
6+
cloudModeEnabled: boolean;
7+
hasGithubIntegration: boolean;
8+
lastUsedLocalWorkspaceMode: LocalWorkspaceMode;
9+
}
10+
11+
// Cloud is only honoured when it works out of the box (flag on + GitHub
12+
// connected); otherwise the preference falls back to the last local mode so
13+
// users never start behind a connect-GitHub prompt.
14+
export function resolveWorkspaceModePreference({
15+
preferredMode,
16+
cloudModeEnabled,
17+
hasGithubIntegration,
18+
lastUsedLocalWorkspaceMode,
19+
}: WorkspaceModePreferenceInput): WorkspaceMode {
20+
if (preferredMode !== "cloud") return preferredMode;
21+
if (!cloudModeEnabled || !hasGithubIntegration) {
22+
return lastUsedLocalWorkspaceMode;
23+
}
24+
return "cloud";
25+
}

0 commit comments

Comments
 (0)