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
1 change: 1 addition & 0 deletions apps/mobile/src/components/AppSymbol.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ const ANDROID_ICON_BY_SF_SYMBOL = {
"bolt.horizontal.circle": IconBolt,
brain: IconBrain,
camera: IconCamera,
"chart.bar": IconChartBar,
"chart.bar.xaxis": IconChartBar,
checkmark: IconCheck,
"checkmark.circle": IconCircleCheck,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,27 @@ function ServerSettingsDetail(props: { readonly page: SettingsPage }) {
(target) =>
target.environment.serverConfig.environment.capabilities.threadRestartContinuation === true,
);

const telemetryOverridden = targets.some(
(target) => target.environment.serverConfig.telemetryDisabledByEnvironment === true,
);

const telemetryPartiallyOverridden =
telemetryOverridden &&
targets.some(
(target) => target.environment.serverConfig.telemetryDisabledByEnvironment !== true,
);

const telemetryValue = telemetryPartiallyOverridden
? null
: !telemetryOverridden && uniform("telemetryEnabled");

const telemetryDescription = telemetryPartiallyOverridden
? "Disabled by some selected servers' environment configuration."
: telemetryOverridden
? "Disabled by the server's environment configuration."
: "Share anonymous usage data to help improve T3 Code.";

const disabledFor = (key: string) =>
disabled ||
(projectSelected &&
Expand Down Expand Up @@ -449,6 +470,23 @@ function ServerSettingsDetail(props: { readonly page: SettingsPage }) {
))}
</SettingsSection>
) : null}
<SettingsSection title="Privacy">
<SettingsSwitchRow
icon="chart.bar"
label="Anonymous analytics"
subtitle={
projectSelected
? "Environment-wide setting. Select All projects to change it."
: telemetryDescription
}
value={telemetryValue}
mixedValue={false}
disabled={telemetryOverridden || disabledFor("telemetryEnabled")}
onValueChange={(value) => {
write({ telemetryEnabled: value });
Comment thread
wukko marked this conversation as resolved.
}}
/>
</SettingsSection>
<SettingsSection title="Updates">
<SettingsSwitchRow
icon="arrow.clockwise"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import { SettingsControlRow } from "./SettingsControlRow";
export function SettingsSwitchRow(
props: Omit<ComponentProps<typeof SettingsControlRow>, "children"> & {
readonly value: boolean | null;
readonly mixedValue?: boolean;
readonly onValueChange: (value: boolean) => void;
},
) {
const mixedValue = props.mixedValue ?? true;
const mixedLabel = mixedValue ? "on" : "off";
return (
<SettingsControlRow
disabled={props.disabled}
Expand All @@ -20,13 +23,13 @@ export function SettingsSwitchRow(
>
{props.value === null ? (
<Pressable
accessibilityLabel={`Set ${props.label} on for selected environments`}
accessibilityLabel={`Set ${props.label} ${mixedLabel} for selected environments`}
accessibilityRole="button"
disabled={props.disabled}
className="rounded-full bg-subtle px-3 py-2 active:opacity-70"
onPress={() => props.onValueChange(true)}
onPress={() => props.onValueChange(mixedValue)}
>
<Text className="text-sm font-t3-medium text-foreground">Mixed · Set on</Text>
<Text className="text-sm font-t3-medium text-foreground">Mixed · Set {mixedLabel}</Text>
</Pressable>
) : (
<ThemedSwitch
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/device/DeviceService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ const fixture = Effect.fn("fixture")(function* (
})),
streamChanges: Stream.empty,
subscribeChanges: Effect.succeed(Stream.empty),
subscribePersistedChanges: Effect.succeed(Stream.empty),
}),
),
Effect.provideService(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,9 @@ const makeHarness = Effect.fn("makeThreadSettlementHarness")(function* (options:
updateProviderInstance: () => Effect.die("Unexpected provider mutation"),
withSettingsSnapshot: (use) => Ref.get(settings).pipe(Effect.flatMap(use)),
streamChanges: Stream.fromPubSub(settingsChanges),
subscribePersistedChanges: PubSub.subscribe(settingsChanges).pipe(
Effect.map(Stream.fromSubscription),
),
subscribeChanges: PubSub.subscribe(settingsChanges).pipe(
Effect.map((subscription) => Stream.fromSubscription(subscription)),
),
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/provider/ProviderRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,9 @@ function makeMutableServerSettingsService(
get streamChanges() {
return Stream.fromPubSub(changes);
},
get subscribePersistedChanges() {
return PubSub.subscribe(changes).pipe(Effect.map(Stream.fromSubscription));
},
get subscribeChanges() {
return PubSub.subscribe(changes).pipe(
Effect.map((subscription) => Stream.fromSubscription(subscription)),
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/provider/makeManagedServerProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,9 @@ describe("makeManagedServerProvider", () => {
updateProviderInstance: () => Effect.die(new Error("unused in this test")),
withSettingsSnapshot: (use) => Ref.get(serverSettingsRef).pipe(Effect.flatMap(use)),
streamChanges: Stream.empty,
subscribePersistedChanges: PubSub.subscribe(serverSettingsChanges).pipe(
Effect.map(Stream.fromSubscription),
),
subscribeChanges: PubSub.subscribe(serverSettingsChanges).pipe(
Effect.map((subscription) => Stream.fromSubscription(subscription)),
),
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ const layerRuntimeDependencies = layerRuntimeCoreDependencies.pipe(
Layer.provideMerge(layerResourceDiagnostics),
Layer.provideMerge(layerUsage),
Layer.provideMerge(TraceDiagnostics.layer),
Layer.provideMerge(AnalyticsService.layer),
Layer.provideMerge(AnalyticsService.layer.pipe(Layer.provide(layerServerSettings))),
Layer.provideMerge(ExternalLauncher.layer),
Layer.provideMerge(RemoteOpenTargets.layer),
Layer.provideMerge(DirectEndpoints.layer),
Expand Down
20 changes: 20 additions & 0 deletions apps/server/src/serverSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,26 @@ const recordProviderUsage = (provider: string, instanceId: string | null = provi
});

it.layer(NodeServices.layer)("server settings", (it) => {
it.effect("persists the analytics opt-out and allows opting back in", () =>
Effect.gen(function* () {
const service = yield* ServerSettingsModule.ServerSettingsService;
const config = yield* ServerConfig.ServerConfig;
const fs = yield* FileSystem.FileSystem;
assert.isTrue((yield* service.getSettings).telemetryEnabled);

for (const telemetryEnabled of [false, true]) {
yield* service.updateSettings({ telemetryEnabled });

const persisted = yield* decodeServerSettingsJson(
yield* fs.readFileString(config.settingsPath),
);

assert.equal(persisted.telemetryEnabled, telemetryEnabled);
assert.equal((yield* service.getSettings).telemetryEnabled, telemetryEnabled);
}
}).pipe(Effect.provide(layerServerSettings())),
);

it.effect("migrates saved token delivery to paragraph buffering without resetting settings", () =>
Effect.gen(function* () {
const config = yield* ServerConfig.ServerConfig;
Expand Down
19 changes: 17 additions & 2 deletions apps/server/src/serverSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,13 @@ export class ServerSettingsService extends Context.Service<
* snapshot and a lazily started stream must not be lost.
*/
readonly subscribeChanges: Effect.Effect<Stream.Stream<ServerSettings>, never, Scope.Scope>;

/** Subscribe without loading secrets, for settings that must react before secret-store I/O. */
readonly subscribePersistedChanges: Effect.Effect<
Stream.Stream<ServerSettings>,
never,
Scope.Scope
>;
}
>()("t3/serverSettings/ServerSettingsService") {
/** @deprecated Import and use `layerTest` from this module. */
Expand All @@ -309,6 +316,7 @@ const makeTest = (overrides: DeepPartial<ServerSettings> = {}) =>
: {}),
});
const currentSettingsRef = yield* Ref.make<ServerSettings>(initialSettings);
const changesPubSub = yield* PubSub.unbounded<ServerSettings>();
const writeSemaphore = yield* Semaphore.make(1);
const getSettings = Ref.get(currentSettingsRef).pipe(Effect.map(resolveTextGenerationProvider));

Expand All @@ -320,6 +328,7 @@ const makeTest = (overrides: DeepPartial<ServerSettings> = {}) =>
Effect.flatMap(update),
Effect.flatMap(normalizeServerSettings),
Effect.tap((nextSettings) => Ref.set(currentSettingsRef, nextSettings)),
Effect.tap((nextSettings) => PubSub.publish(changesPubSub, nextSettings)),
Effect.map(resolveTextGenerationProvider),
),
);
Expand All @@ -346,8 +355,11 @@ const makeTest = (overrides: DeepPartial<ServerSettings> = {}) =>
),
withSettingsSnapshot: (use) =>
writeSemaphore.withPermits(1)(getSettings.pipe(Effect.flatMap(use))),
streamChanges: Stream.empty,
subscribeChanges: Effect.succeed(Stream.empty),
streamChanges: Stream.fromPubSub(changesPubSub),
subscribeChanges: PubSub.subscribe(changesPubSub).pipe(Effect.map(Stream.fromSubscription)),
subscribePersistedChanges: PubSub.subscribe(changesPubSub).pipe(
Effect.map(Stream.fromSubscription),
),
} satisfies ServerSettingsService["Service"];
});

Expand Down Expand Up @@ -1390,6 +1402,9 @@ const make = Effect.gen(function* () {
Effect.map((subscription) => materializeChanges(Stream.fromSubscription(subscription))),
);
},
get subscribePersistedChanges() {
return PubSub.subscribe(changesPubSub).pipe(Effect.map(Stream.fromSubscription));
},
} satisfies ServerSettingsService["Service"];
});

Expand Down
Loading
Loading