Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d42da49
fix(web): warn when an environment silently drops a setting
flamboh Oct 11, 2026
b2dcb63
fix(web): catch nested setting keys dropped beside saved ones
flamboh Oct 11, 2026
92ef200
feat(settings): leave environments that cannot store a setting out of…
flamboh Oct 11, 2026
76e4607
refactor(web): read worktree cleanup from the environment's filtered …
flamboh Oct 11, 2026
382a2e9
Merge remote-tracking branch 'origin/main' into t3/settings-unsupport…
flamboh Oct 13, 2026
d13abdb
Merge origin/main into t3/settings-unsupported-environments
flamboh Oct 16, 2026
09376ca
fix(settings): disable unsupported children editors
flamboh Oct 16, 2026
f14fb70
test(settings): cover unsupported environments and partial saves
juliusmarminge Oct 19, 2026
ba56baa
chore: merge main into unsupported settings takeover
juliusmarminge Oct 19, 2026
00a6a4b
fix(settings): filter unsupported targets without disabling supported…
juliusmarminge Oct 19, 2026
b8a5244
test(settings): restore Chromium coverage and catch dropped actions
juliusmarminge Oct 19, 2026
7c4909c
fix(settings): honor supported targets in custom editors
juliusmarminge Oct 19, 2026
8acd4f1
test(web): cover unsupported project settings destinations
juliusmarminge Oct 19, 2026
ac322ca
fix(settings): align project reads and permissions with supported tar…
juliusmarminge Oct 19, 2026
a5c98e7
fix(settings): allow device host edits with offline environments
juliusmarminge Oct 19, 2026
2db26f2
test(web): cover project setting drops across scopes
juliusmarminge Oct 19, 2026
6ad63d0
fix(settings): isolate learned project override support
juliusmarminge Oct 19, 2026
a65c70d
test(settings): cover model picker support eligibility
juliusmarminge Oct 19, 2026
4e5d197
fix(settings): use eligible model provider catalogs
juliusmarminge Oct 19, 2026
b0809aa
merge: integrate main into settings support branch
juliusmarminge Oct 19, 2026
c3f7138
test(settings): reject partially saved project actions
juliusmarminge Oct 19, 2026
9d28fcf
fix(web): reject partially saved project actions
juliusmarminge Oct 19, 2026
03c33f7
merge: integrate main into settings support branch
juliusmarminge Oct 19, 2026
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
1 change: 1 addition & 0 deletions apps/server/src/environment/ServerEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
expect(second.capabilities.attachmentUploads).toBe(true);
expect(second.capabilities.fileAttachments).toEqual({ maxUploadBytes: 50 * 1024 * 1024 });
expect(second.capabilities.pullRequests).toBe(true);
expect(second.capabilities.settingsKeys).toContain("reviewGuidesEnabled");
expect(second.capabilities.requiredWorktreeBootstrap).toBe(true);
expect(second.capabilities.usagePriceOverrides).toBe(true);
expect(second.capabilities.threadActiveReorder).toBe(true);
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/environment/ServerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
EnvironmentId,
ORCHESTRATION_PROTOCOL_VERSION,
PROVIDER_SEND_TURN_MAX_FILE_BYTES,
ServerSettings,
type ExecutionEnvironmentDescriptor,
} from "@t3tools/contracts";
import * as HostProcess from "@t3tools/shared/HostProcess";
Expand Down Expand Up @@ -253,6 +254,7 @@ export const make = Effect.gen(function* () {
serverResolvedCommandContext: true,
environmentIcon: true,
projectCloneTracking: true,
settingsKeys: Object.keys(ServerSettings.fields),
...(serverSelfUpdate === null ? {} : { serverSelfUpdate }),
...(serverInstallation === null ? {} : { serverInstallation }),
// V2 restart recovery uses the environment-owned opt-in. The old
Expand Down
77 changes: 42 additions & 35 deletions apps/web/src/components/settings/SettingInheritance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ export function SettingInheritance({
target,
environment: { ...environment, serverConfig: environment.serverConfig },
machine: resolveEnvironmentMachineKind(environment.serverConfig),
unsupported: target.unsupportedSettingKeys?.has(key) === true,
layers: settingInheritanceLayers(target, environment.serverConfig.settings, key),
},
];
Expand Down Expand Up @@ -227,7 +228,7 @@ export function SettingInheritance({
</Tooltip>
<PopoverPopup align="start" width="md" padding="none">
<div className="divide-y divide-border/60">
{chains.map(({ target, environment, machine, layers }) => (
{chains.map(({ target, environment, machine, unsupported, layers }) => (
<section
key={`${target.environmentId}:${target.projectId ?? ""}`}
className="px-3 py-2.5"
Expand All @@ -236,43 +237,49 @@ export function SettingInheritance({
<EnvironmentMachineIcon aria-hidden kind={machine} className="size-3.5 shrink-0" />
<span className="min-w-0 truncate">{target.label}</span>
</h4>
<ol role="list" className="text-sm">
{layers.map((layer) => (
<li
key={layer.key}
className={cn(
"grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 rounded-md px-2 py-1",
layer.effective && "bg-foreground/[0.06]",
)}
>
<span
{unsupported ? (
<p className="px-2 py-1 text-sm text-muted-foreground">
Not supported. Update T3 Code on this environment to use this setting.
</p>
) : (
<ol role="list" className="text-sm">
{layers.map((layer) => (
<li
key={layer.key}
className={cn(
"min-w-0 truncate",
layer.effective ? "font-medium text-foreground" : "text-muted-foreground",
"grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 rounded-md px-2 py-1",
layer.effective && "bg-foreground/[0.06]",
)}
>
{layer.key === "environment" ? "Environment" : layer.label}
</span>
<span
className={cn(
"flex items-center gap-1.5 tabular-nums",
layer.effective
? "text-foreground"
: layer.set
? "text-muted-foreground"
: "text-muted-foreground/60",
)}
>
<span className="max-w-32 truncate">{layer.value}</span>
{layer.effective ? (
<CheckIcon aria-hidden className="size-3.5 shrink-0 text-primary" />
) : (
<span aria-hidden className="size-3.5 shrink-0" />
)}
</span>
</li>
))}
</ol>
<span
className={cn(
"min-w-0 truncate",
layer.effective ? "font-medium text-foreground" : "text-muted-foreground",
)}
>
{layer.key === "environment" ? "Environment" : layer.label}
</span>
<span
className={cn(
"flex items-center gap-1.5 tabular-nums",
layer.effective
? "text-foreground"
: layer.set
? "text-muted-foreground"
: "text-muted-foreground/60",
)}
>
<span className="max-w-32 truncate">{layer.value}</span>
{layer.effective ? (
<CheckIcon aria-hidden className="size-3.5 shrink-0 text-primary" />
) : (
<span aria-hidden className="size-3.5 shrink-0" />
)}
</span>
</li>
))}
</ol>
)}
{(() => {
const overriding = overridingProjects.filter(
(project) => project.environmentId === target.environmentId,
Expand Down
24 changes: 22 additions & 2 deletions apps/web/src/components/settings/SettingsScopeContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import { createContext, type ReactNode, useContext, useMemo } from "react";
import { useEnvironments, usePrimaryEnvironmentId } from "../../state/environments";
import { getProjectFileQueryAtom, optimisticFileAtom } from "../files/projectFilesQueryState";
import { useSettingsProjectGroups } from "./useSettingsProjectGroups";
import { resolveScopedSettingsTargets, selectScopedSettingsEnvironments } from "./scopedSettings";
import { droppedSettingKeysAtom, droppedSettingKeysFor } from "./droppedSettingKeys";
import {
resolveScopedSettingsTargets,
resolveUnsupportedSettingKeys,
selectScopedSettingsEnvironments,
} from "./scopedSettings";
import { resolveSettingsScope, type SettingsScopeSearch } from "./settingsScope";
import { selectSingleEnvironmentScope } from "./settingsScopeAxis";

Expand Down Expand Up @@ -56,7 +61,22 @@ function useMemberProjectFiles(scope: ReturnType<typeof resolveSettingsScope>) {

function useResolvedSettingsScope(rawSearch: SettingsScopeSearch, singleEnvironment: boolean) {
const groups = useSettingsProjectGroups();
const { environments: availableEnvironments } = useEnvironments();
const { environments } = useEnvironments();
const dropped = useAtomValue(droppedSettingKeysAtom);
const availableEnvironments = useMemo(
() =>
environments.map((environment) => {
const descriptor = environment.serverConfig?.environment;
const unsupportedSettingKeys = resolveUnsupportedSettingKeys(
descriptor?.capabilities.settingsKeys,
droppedSettingKeysFor(dropped, environment.environmentId, descriptor?.serverVersion),
);
return unsupportedSettingKeys.size === 0
? environment
: { ...environment, unsupportedSettingKeys };
}),
[dropped, environments],
);
const primaryEnvironmentId = usePrimaryEnvironmentId();
const search = useMemo(
() =>
Expand Down
45 changes: 45 additions & 0 deletions apps/web/src/components/settings/droppedSettingKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { EnvironmentId } from "@t3tools/contracts";
import { Atom } from "effect/reactivity";

import { appAtomRegistry } from "../../rpc/atomRegistry";

interface DroppedSettingKeys {
readonly serverVersion: string;
readonly keys: ReadonlySet<string>;
}

/**
* Setting keys each environment dropped on a save this session, for servers
* too old to list the keys they store. Tagged with the server version, so an
* update clears them.
*/
export const droppedSettingKeysAtom = Atom.make<ReadonlyMap<EnvironmentId, DroppedSettingKeys>>(
new Map(),
).pipe(Atom.keepAlive, Atom.withLabel("settings:dropped-setting-keys"));

export function recordDroppedSettingKeys(
environmentId: EnvironmentId,
serverVersion: string,
keys: readonly string[],
) {
if (keys.length === 0) return;
appAtomRegistry.update(droppedSettingKeysAtom, (current) => {
const existing = current.get(environmentId);
const next = new Map(current);
next.set(environmentId, {
serverVersion,
keys: new Set([...(existing?.serverVersion === serverVersion ? existing.keys : []), ...keys]),
});
return next;
});
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/** The keys an environment dropped, if it still runs the version that dropped them. */
export function droppedSettingKeysFor(
dropped: ReadonlyMap<EnvironmentId, DroppedSettingKeys>,
environmentId: EnvironmentId,
serverVersion: string | undefined,
): ReadonlySet<string> | undefined {
const entry = dropped.get(environmentId);
return entry && entry.serverVersion === serverVersion ? entry.keys : undefined;
}
Loading
Loading