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
2 changes: 2 additions & 0 deletions apps/server/src/checkpointing/Layers/CheckpointStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import { GitCoreLive } from "../../git/Layers/GitCore.ts";
import { GitCore } from "../../git/Services/GitCore.ts";
import { GitCommandError } from "@t3tools/contracts";
import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { ThreadId } from "@t3tools/contracts";

const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), {
prefix: "t3-checkpoint-store-test-",
});
const GitCoreTestLayer = GitCoreLive.pipe(
Layer.provide(ServerConfigLayer),
Layer.provide(ServerSettingsService.layerTest()),
Layer.provide(NodeServices.layer),
);
const CheckpointStoreTestLayer = CheckpointStoreLive.pipe(
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/git/Layers/GitCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import { GitCore, type GitCoreShape } from "../Services/GitCore.ts";
import { GitCommandError } from "@t3tools/contracts";
import { type ProcessRunResult, runProcess } from "../../processRunner.ts";
import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";

// ── Helpers ──

const ServerConfigLayer = ServerConfig.layerTest(process.cwd(), { prefix: "t3-git-core-test-" });
const GitCoreTestLayer = GitCoreLive.pipe(
Layer.provide(ServerConfigLayer),
Layer.provide(ServerSettingsService.layerTest()),
Layer.provide(NodeServices.layer),
);
const TestLayer = Layer.mergeAll(NodeServices.layer, GitCoreTestLayer);
Expand Down
52 changes: 38 additions & 14 deletions apps/server/src/git/Layers/GitCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
parseRemoteRefWithRemoteNames,
} from "../remoteRefs.ts";
import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { decodeJsonResult } from "@t3tools/shared/schemaJson";

const DEFAULT_TIMEOUT_MS = 30_000;
Expand Down Expand Up @@ -74,6 +75,7 @@ interface ExecuteGitOptions {
maxOutputBytes?: number | undefined;
truncateOutputAtMaxBytes?: boolean | undefined;
progress?: ExecuteGitProgress | undefined;
env?: Record<string, string> | undefined;
}

function parseBranchAb(value: string): { ahead: number; behind: number } {
Expand Down Expand Up @@ -632,6 +634,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const { worktreesDir } = yield* ServerConfig;
const serverSettings = yield* ServerSettingsService;

let executeRaw: GitCoreShape["execute"];

Expand Down Expand Up @@ -770,6 +773,7 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: {
cwd,
args,
...(options.stdin !== undefined ? { stdin: options.stdin } : {}),
...(options.env !== undefined ? { env: options.env } : {}),
allowNonZeroExit: true,
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
...(options.maxOutputBytes !== undefined ? { maxOutputBytes: options.maxOutputBytes } : {}),
Expand Down Expand Up @@ -893,20 +897,35 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: {
const fetchUpstreamRefForStatus = (
gitCommonDir: string,
upstream: { upstreamRef: string; remoteName: string; upstreamBranch: string },
): Effect.Effect<void, GitCommandError> => {
const refspec = `+refs/heads/${upstream.upstreamBranch}:refs/remotes/${upstream.upstreamRef}`;
const fetchCwd =
path.basename(gitCommonDir) === ".git" ? path.dirname(gitCommonDir) : gitCommonDir;
return executeGit(
"GitCore.fetchUpstreamRefForStatus",
fetchCwd,
["--git-dir", gitCommonDir, "fetch", "--quiet", "--no-tags", upstream.remoteName, refspec],
{
allowNonZeroExit: true,
timeoutMs: Duration.toMillis(STATUS_UPSTREAM_REFRESH_TIMEOUT),
},
).pipe(Effect.asVoid);
};
): Effect.Effect<void, GitCommandError> =>
Effect.gen(function* () {
const settings = yield* serverSettings.getSettings.pipe(
Effect.catch(() => Effect.succeed(null)),
);
const quietEnv: Record<string, string> =
settings?.gitQuietBackgroundChecks
? {
GIT_TERMINAL_PROMPT: "0",
SSH_ASKPASS: "",
GIT_ASKPASS: "",
GCM_INTERACTIVE: "never",
}
: {};

const refspec = `+refs/heads/${upstream.upstreamBranch}:refs/remotes/${upstream.upstreamRef}`;
const fetchCwd =
path.basename(gitCommonDir) === ".git" ? path.dirname(gitCommonDir) : gitCommonDir;
yield* executeGit(
"GitCore.fetchUpstreamRefForStatus",
fetchCwd,
["--git-dir", gitCommonDir, "fetch", "--quiet", "--no-tags", upstream.remoteName, refspec],
{
allowNonZeroExit: true,
timeoutMs: Duration.toMillis(STATUS_UPSTREAM_REFRESH_TIMEOUT),
env: quietEnv,
},
);
}).pipe(Effect.asVoid);

const resolveGitCommonDir = Effect.fn("resolveGitCommonDir")(function* (cwd: string) {
const gitCommonDir = yield* runGitStdout("GitCore.resolveGitCommonDir", cwd, [
Expand Down Expand Up @@ -940,6 +959,11 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: {
const refreshStatusUpstreamIfStale = Effect.fn("refreshStatusUpstreamIfStale")(function* (
cwd: string,
) {
const settings = yield* serverSettings.getSettings.pipe(
Effect.catch(() => Effect.succeed(null)),
);
if (settings?.gitPollingMode === "disabled") return;

const upstream = yield* resolveCurrentUpstream(cwd);
if (!upstream) return;
const gitCommonDir = yield* resolveGitCommonDir(cwd);
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/git/Layers/GitManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,7 @@ function makeManager(input?: {
const gitCoreLayer = GitCoreLive.pipe(
Layer.provideMerge(NodeServices.layer),
Layer.provideMerge(ServerConfigLayer),
Layer.provideMerge(serverSettingsLayer),
);

const managerLayer = Layer.mergeAll(
Expand All @@ -649,6 +650,7 @@ const asThreadId = (threadId: string) => threadId as ThreadId;

const GitManagerTestLayer = GitCoreLive.pipe(
Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-git-manager-test-" })),
Layer.provide(ServerSettingsService.layerTest()),
Layer.provideMerge(NodeServices.layer),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { CheckpointStoreLive } from "../../checkpointing/Layers/CheckpointStore.ts";
import { CheckpointStore } from "../../checkpointing/Services/CheckpointStore.ts";
import { GitCoreLive } from "../../git/Layers/GitCore.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { CheckpointReactorLive } from "./CheckpointReactor.ts";
import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";
import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts";
Expand Down Expand Up @@ -271,6 +272,7 @@ describe("CheckpointReactor", () => {
Layer.provideMerge(WorkspaceEntriesLive.pipe(Layer.provide(WorkspacePathsLive))),
Layer.provideMerge(WorkspacePathsLive),
Layer.provideMerge(GitCoreLive),
Layer.provideMerge(ServerSettingsService.layerTest()),
Layer.provideMerge(ServerConfigLayer),
Layer.provideMerge(NodeServices.layer),
);
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/workspace/Layers/WorkspaceEntries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Effect, FileSystem, Layer, Path, PlatformError } from "effect";

import { ServerConfig } from "../../config.ts";
import { GitCoreLive } from "../../git/Layers/GitCore.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { GitCore } from "../../git/Services/GitCore.ts";
import { WorkspaceEntries } from "../Services/WorkspaceEntries.ts";
import { WorkspaceEntriesLive } from "./WorkspaceEntries.ts";
Expand All @@ -15,6 +16,7 @@ const TestLayer = Layer.empty.pipe(
Layer.provideMerge(WorkspaceEntriesLive.pipe(Layer.provide(WorkspacePathsLive))),
Layer.provideMerge(WorkspacePathsLive),
Layer.provideMerge(GitCoreLive),
Layer.provideMerge(ServerSettingsService.layerTest()),
Layer.provide(
ServerConfig.layerTest(process.cwd(), {
prefix: "t3-workspace-entries-test-",
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/workspace/Layers/WorkspaceFileSystem.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Effect, FileSystem, Layer, Path } from "effect";

import { ServerConfig } from "../../config.ts";
import { GitCoreLive } from "../../git/Layers/GitCore.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { WorkspaceEntries } from "../Services/WorkspaceEntries.ts";
import { WorkspaceFileSystem } from "../Services/WorkspaceFileSystem.ts";
import { WorkspaceEntriesLive } from "./WorkspaceEntries.ts";
Expand All @@ -20,6 +21,7 @@ const TestLayer = Layer.empty.pipe(
Layer.provideMerge(WorkspaceEntriesLive.pipe(Layer.provide(WorkspacePathsLive))),
Layer.provideMerge(WorkspacePathsLive),
Layer.provideMerge(GitCoreLive),
Layer.provideMerge(ServerSettingsService.layerTest()),
Layer.provide(
ServerConfig.layerTest(process.cwd(), {
prefix: "t3-workspace-files-test-",
Expand Down
16 changes: 13 additions & 3 deletions apps/web/src/components/BranchToolbarBranchSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
gitStatusQueryOptions,
invalidateGitQueries,
} from "../lib/gitReactQuery";
import { useSettings } from "../hooks/useSettings";
import { readNativeApi } from "../nativeApi";
import { parsePullRequestReference } from "../pullRequestReference";
import {
Expand Down Expand Up @@ -89,16 +90,24 @@ export function BranchToolbarBranchSelector({
const [branchQuery, setBranchQuery] = useState("");
const deferredBranchQuery = useDeferredValue(branchQuery);

const branchStatusQuery = useQuery(gitStatusQueryOptions(branchCwd));
const { gitPollingMode } = useSettings();
const gitPollingEnabled = gitPollingMode !== "disabled";
const branchStatusQuery = useQuery(
gitStatusQueryOptions(branchCwd, { pollingEnabled: gitPollingEnabled }),
);
const trimmedBranchQuery = branchQuery.trim();
const deferredTrimmedBranchQuery = deferredBranchQuery.trim();

useEffect(() => {
if (!branchCwd) return;
void queryClient.prefetchInfiniteQuery(
gitBranchSearchInfiniteQueryOptions({ cwd: branchCwd, query: "" }),
gitBranchSearchInfiniteQueryOptions({
cwd: branchCwd,
query: "",
pollingEnabled: gitPollingEnabled,
}),
);
}, [branchCwd, queryClient]);
}, [branchCwd, queryClient, gitPollingEnabled]);

const {
data: branchesSearchData,
Expand All @@ -111,6 +120,7 @@ export function BranchToolbarBranchSelector({
cwd: branchCwd,
query: deferredTrimmedBranchQuery,
enabled: isBranchMenuOpen,
pollingEnabled: gitPollingEnabled,
}),
);
const branches = useMemo(
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1399,7 +1399,10 @@ export default function ChatView({ threadId }: ChatViewProps) {
(debouncerState) => ({ isPending: debouncerState.isPending }),
);
const effectivePathQuery = pathTriggerQuery.length > 0 ? debouncedPathQuery : "";
const gitStatusQuery = useQuery(gitStatusQueryOptions(gitCwd));
const gitPollingEnabled = settings.gitPollingMode !== "disabled";
const gitStatusQuery = useQuery(
gitStatusQueryOptions(gitCwd, { pollingEnabled: gitPollingEnabled }),
);
const keybindings = useServerKeybindings();
const availableEditors = useServerAvailableEditors();
const modelOptionsByProvider = useMemo(
Expand Down
6 changes: 5 additions & 1 deletion apps/web/src/components/DiffPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,11 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
activeProjectId ? store.projects.find((project) => project.id === activeProjectId) : undefined,
);
const activeCwd = activeThread?.worktreePath ?? activeProject?.cwd;
const gitStatusQuery = useQuery(gitStatusQueryOptions(activeCwd ?? null));
const gitStatusQuery = useQuery(
gitStatusQueryOptions(activeCwd ?? null, {
pollingEnabled: settings.gitPollingMode !== "disabled",
}),
);
const isGitRepo = gitStatusQuery.data?.isRepo ?? true;
const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } =
useTurnDiffSummaries(activeThread);
Expand Down
6 changes: 5 additions & 1 deletion apps/web/src/components/GitActionsControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
import { newCommandId, randomUUID } from "~/lib/utils";
import { resolvePathLinkTarget } from "~/terminal-links";
import { readNativeApi } from "~/nativeApi";
import { useSettings } from "~/hooks/useSettings";
import { useStore } from "~/store";

interface GitActionsControlProps {
Expand Down Expand Up @@ -275,7 +276,10 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
[persistThreadBranchSync],
);

const { data: gitStatus = null, error: gitStatusError } = useQuery(gitStatusQueryOptions(gitCwd));
const { gitPollingMode } = useSettings();
const { data: gitStatus = null, error: gitStatusError } = useQuery(
gitStatusQueryOptions(gitCwd, { pollingEnabled: gitPollingMode !== "disabled" }),
);
// Default to true while loading so we don't flash init controls.
const isRepo = gitStatus?.isRepo ?? true;
const hasOriginRemote = gitStatus?.hasOriginRemote ?? false;
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -782,11 +782,12 @@ export default function Sidebar() {
],
[threadGitTargets],
);
const sidebarGitPollingEnabled = appSettings.gitPollingMode === "all";
const threadGitStatusQueries = useQueries({
queries: threadGitStatusCwds.map((cwd) => ({
...gitStatusQueryOptions(cwd),
...gitStatusQueryOptions(cwd, { pollingEnabled: sidebarGitPollingEnabled }),
staleTime: 30_000,
refetchInterval: 60_000,
refetchInterval: sidebarGitPollingEnabled ? 60_000 : false,
})),
});
const prByThreadId = useMemo(() => {
Expand Down
80 changes: 80 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ const TIMESTAMP_FORMAT_LABELS = {
"24-hour": "24-hour",
} as const;

const GIT_POLLING_MODE_LABELS = {
all: "All sessions",
"active-session": "Active session only",
disabled: "Disabled",
} as const;

type InstallProviderSettings = {
provider: ProviderKind;
title: string;
Expand Down Expand Up @@ -1411,6 +1417,80 @@ export function GeneralSettingsPanel() {
})}
</SettingsSection>

<SettingsSection title="Git Sync">
<SettingsRow
title="Background git polling"
description="Controls how t3code automatically fetches from remote and refreshes git status. Disabling stops all automatic git sync, including ahead/behind counts."
resetAction={
settings.gitPollingMode !== DEFAULT_UNIFIED_SETTINGS.gitPollingMode ? (
<SettingResetButton
label="background git polling"
onClick={() =>
updateSettings({
gitPollingMode: DEFAULT_UNIFIED_SETTINGS.gitPollingMode,
})
}
/>
) : null
}
control={
<Select
value={settings.gitPollingMode}
onValueChange={(value) => {
if (value === "all" || value === "active-session" || value === "disabled") {
updateSettings({ gitPollingMode: value });
}
}}
>
<SelectTrigger className="w-full sm:w-48" aria-label="Git polling mode">
<SelectValue>
{GIT_POLLING_MODE_LABELS[settings.gitPollingMode]}
</SelectValue>
</SelectTrigger>
<SelectPopup align="end" alignItemWithTrigger={false}>
<SelectItem hideIndicator value="all">
{GIT_POLLING_MODE_LABELS.all}
</SelectItem>
<SelectItem hideIndicator value="active-session">
{GIT_POLLING_MODE_LABELS["active-session"]}
</SelectItem>
<SelectItem hideIndicator value="disabled">
{GIT_POLLING_MODE_LABELS.disabled}
</SelectItem>
</SelectPopup>
</Select>
}
/>

<SettingsRow
title="Quiet background fetches"
description="Suppress credential prompts during automatic git fetches. Useful with SSH agents like 1Password that require interactive confirmation per request."
resetAction={
settings.gitQuietBackgroundChecks !==
DEFAULT_UNIFIED_SETTINGS.gitQuietBackgroundChecks ? (
<SettingResetButton
label="quiet background fetches"
onClick={() =>
updateSettings({
gitQuietBackgroundChecks: DEFAULT_UNIFIED_SETTINGS.gitQuietBackgroundChecks,
})
}
/>
) : null
}
control={
<Switch
checked={settings.gitQuietBackgroundChecks}
disabled={settings.gitPollingMode === "disabled"}
onCheckedChange={(checked) =>
updateSettings({ gitQuietBackgroundChecks: Boolean(checked) })
}
aria-label="Suppress credential prompts during background fetches"
/>
}
/>
</SettingsSection>

<SettingsSection title="Advanced">
<SettingsRow
title="Keybindings"
Expand Down
Loading
Loading