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
14 changes: 14 additions & 0 deletions apps/web/src/appSettings.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { describe, expect, it } from "vitest";

import {
DEFAULT_CODEX_FAST_MODE,
DEFAULT_CODEX_REASONING_EFFORT,
DEFAULT_TIMESTAMP_FORMAT,
getAppModelOptions,
normalizeCustomModelSlugs,
Expand Down Expand Up @@ -64,3 +66,15 @@ describe("timestamp format defaults", () => {
expect(DEFAULT_TIMESTAMP_FORMAT).toBe("locale");
});
});

describe("reasoning defaults", () => {
it("defaults Codex reasoning to the built-in high level", () => {
expect(DEFAULT_CODEX_REASONING_EFFORT).toBe("high");
});
});

describe("fast mode defaults", () => {
it("defaults Codex fast mode to off", () => {
expect(DEFAULT_CODEX_FAST_MODE).toBe(false);
});
});
24 changes: 22 additions & 2 deletions apps/web/src/appSettings.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { useCallback } from "react";
import { Option, Schema } from "effect";
import { type ProviderKind } from "@t3tools/contracts";
import { getDefaultModel, getModelOptions, normalizeModelSlug } from "@t3tools/shared/model";
import {
CODEX_REASONING_EFFORT_OPTIONS,
type CodexReasoningEffort,
type ProviderKind,
} from "@t3tools/contracts";
import {
getDefaultModel,
getDefaultReasoningEffort,
getModelOptions,
normalizeModelSlug,
} from "@t3tools/shared/model";
import { useLocalStorage } from "./hooks/useLocalStorage";

const APP_SETTINGS_STORAGE_KEY = "t3code:app-settings:v1";
Expand All @@ -10,6 +19,9 @@ export const MAX_CUSTOM_MODEL_LENGTH = 256;
export const TIMESTAMP_FORMAT_OPTIONS = ["locale", "12-hour", "24-hour"] as const;
export type TimestampFormat = (typeof TIMESTAMP_FORMAT_OPTIONS)[number];
export const DEFAULT_TIMESTAMP_FORMAT: TimestampFormat = "locale";
export const DEFAULT_CODEX_REASONING_EFFORT: CodexReasoningEffort =
getDefaultReasoningEffort("codex");
export const DEFAULT_CODEX_FAST_MODE = false;
const BUILT_IN_MODEL_SLUGS_BY_PROVIDER: Record<ProviderKind, ReadonlySet<string>> = {
codex: new Set(getModelOptions("codex").map((option) => option.slug)),
};
Expand All @@ -28,6 +40,14 @@ const AppSettingsSchema = Schema.Struct({
timestampFormat: Schema.Literals(["locale", "12-hour", "24-hour"]).pipe(
Schema.withConstructorDefault(() => Option.some(DEFAULT_TIMESTAMP_FORMAT)),
),
// Kept under the existing storage key so local settings survive the move from
// an explicit settings control to composer-driven last-used persistence.
defaultCodexReasoningEffort: Schema.Literals(CODEX_REASONING_EFFORT_OPTIONS).pipe(
Schema.withConstructorDefault(() => Option.some(DEFAULT_CODEX_REASONING_EFFORT)),
),
lastUsedCodexFastMode: Schema.Boolean.pipe(
Schema.withConstructorDefault(() => Option.some(DEFAULT_CODEX_FAST_MODE)),
),
customCodexModels: Schema.Array(Schema.String).pipe(
Schema.withConstructorDefault(() => Option.some([])),
),
Expand Down
33 changes: 26 additions & 7 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,14 +178,14 @@
threadId: ThreadId;
}

export default function ChatView({ threadId }: ChatViewProps) {

Check warning on line 181 in apps/web/src/components/ChatView.tsx

View workflow job for this annotation

GitHub Actions / Format, Lint, Typecheck, Test, Browser Test, Build

eslint-plugin-unicorn(consistent-function-scoping)

Function `extendReplacementRangeForTrailingSpace` does not capture any variables from its parent scope
const threads = useStore((store) => store.threads);
const projects = useStore((store) => store.projects);
const markThreadVisited = useStore((store) => store.markThreadVisited);
const syncServerReadModel = useStore((store) => store.syncServerReadModel);
const setStoreThreadError = useStore((store) => store.setError);
const setStoreThreadBranch = useStore((store) => store.setThreadBranch);
const { settings } = useAppSettings();
const { settings, updateSettings } = useAppSettings();
const timestampFormat = settings.timestampFormat;
const navigate = useNavigate();
const rawSearch = useSearch({
Expand Down Expand Up @@ -502,9 +502,16 @@
}, [baseThreadModel, composerDraft.model, customModelsForSelectedProvider, selectedProvider]);
const reasoningOptions = getReasoningEffortOptions(selectedProvider);
const supportsReasoningEffort = reasoningOptions.length > 0;
const selectedEffort = composerDraft.effort ?? getDefaultReasoningEffort(selectedProvider);
const defaultCodexReasoningEffort = settings.defaultCodexReasoningEffort;
const selectedEffort =
composerDraft.effort ??
(selectedProvider === "codex"
? defaultCodexReasoningEffort
: getDefaultReasoningEffort(selectedProvider));
const selectedCodexFastModeEnabled =
selectedProvider === "codex" ? composerDraft.codexFastMode : false;
selectedProvider === "codex"
? (composerDraft.codexFastMode ?? settings.lastUsedCodexFastMode)
: false;
const selectedModelOptionsForDispatch = useMemo(() => {
if (selectedProvider !== "codex") {
return undefined;
Expand Down Expand Up @@ -2884,17 +2891,27 @@
);
const onEffortSelect = useCallback(
(effort: CodexReasoningEffort) => {
setComposerDraftEffort(threadId, effort);
updateSettings({
defaultCodexReasoningEffort: effort,
});
// Composer selections now define the global last-used preference, so the
// thread should fall back to that shared setting instead of pinning an override.
setComposerDraftEffort(threadId, null);
scheduleComposerFocus();
},
[scheduleComposerFocus, setComposerDraftEffort, threadId],
[scheduleComposerFocus, setComposerDraftEffort, threadId, updateSettings],
);
const onCodexFastModeChange = useCallback(
(enabled: boolean) => {
setComposerDraftCodexFastMode(threadId, enabled);
updateSettings({
lastUsedCodexFastMode: enabled,
});
// Keep the thread following the global last-used setting after the user
// changes it from the composer.
setComposerDraftCodexFastMode(threadId, null);
scheduleComposerFocus();
},
[scheduleComposerFocus, setComposerDraftCodexFastMode, threadId],
[scheduleComposerFocus, setComposerDraftCodexFastMode, threadId, updateSettings],
);
const onEnvModeChange = useCallback(
(mode: DraftThreadEnvMode) => {
Expand Down Expand Up @@ -3503,6 +3520,7 @@
{isComposerFooterCompact ? (
<CompactComposerControlsMenu
activePlan={Boolean(activePlan || activeProposedPlan || planSidebarOpen)}
defaultEffort={defaultCodexReasoningEffort}
interactionMode={interactionMode}
planSidebarOpen={planSidebarOpen}
runtimeMode={runtimeMode}
Expand All @@ -3525,6 +3543,7 @@
className="mx-0.5 hidden h-4 sm:block"
/>
<CodexTraitsPicker
defaultEffort={defaultCodexReasoningEffort}
effort={selectedEffort}
fastModeEnabled={selectedCodexFastModeEnabled}
options={reasoningOptions}
Expand Down
16 changes: 5 additions & 11 deletions apps/web/src/components/chat/CodexTraitsPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type CodexReasoningEffort } from "@t3tools/contracts";
import { getDefaultReasoningEffort } from "@t3tools/shared/model";
import { memo, useState } from "react";
import { ChevronDownIcon } from "lucide-react";
import { CODEX_REASONING_EFFORT_LABELS } from "@t3tools/contracts";
import { Button } from "../ui/button";
import {
Menu,
Expand All @@ -15,21 +15,15 @@ import {

export const CodexTraitsPicker = memo(function CodexTraitsPicker(props: {
effort: CodexReasoningEffort;
defaultEffort: CodexReasoningEffort;
fastModeEnabled: boolean;
options: ReadonlyArray<CodexReasoningEffort>;
onEffortChange: (effort: CodexReasoningEffort) => void;
onFastModeChange: (enabled: boolean) => void;
}) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const defaultReasoningEffort = getDefaultReasoningEffort("codex");
const reasoningLabelByOption: Record<CodexReasoningEffort, string> = {
low: "Low",
medium: "Medium",
high: "High",
xhigh: "Extra High",
};
const triggerLabel = [
reasoningLabelByOption[props.effort],
CODEX_REASONING_EFFORT_LABELS[props.effort],
...(props.fastModeEnabled ? ["Fast"] : []),
]
.filter(Boolean)
Expand Down Expand Up @@ -68,8 +62,8 @@ export const CodexTraitsPicker = memo(function CodexTraitsPicker(props: {
>
{props.options.map((effort) => (
<MenuRadioItem key={effort} value={effort}>
{reasoningLabelByOption[effort]}
{effort === defaultReasoningEffort ? " (default)" : ""}
{CODEX_REASONING_EFFORT_LABELS[effort]}
{effort === props.defaultEffort ? " (default)" : ""}
</MenuRadioItem>
))}
</MenuRadioGroup>
Expand Down
15 changes: 4 additions & 11 deletions apps/web/src/components/chat/CompactComposerControlsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import {
RuntimeMode,
ProviderInteractionMode,
} from "@t3tools/contracts";
import { getDefaultReasoningEffort } from "@t3tools/shared/model";
import { memo } from "react";
import { EllipsisIcon, ListTodoIcon } from "lucide-react";
import { CODEX_REASONING_EFFORT_LABELS } from "@t3tools/contracts";
import { Button } from "../ui/button";
import {
Menu,
Expand All @@ -21,6 +21,7 @@ import {

export const CompactComposerControlsMenu = memo(function CompactComposerControlsMenu(props: {
activePlan: boolean;
defaultEffort: CodexReasoningEffort;
interactionMode: ProviderInteractionMode;
planSidebarOpen: boolean;
runtimeMode: RuntimeMode;
Expand All @@ -34,14 +35,6 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls
onTogglePlanSidebar: () => void;
onToggleRuntimeMode: () => void;
}) {
const defaultReasoningEffort = getDefaultReasoningEffort("codex");
const reasoningLabelByOption: Record<CodexReasoningEffort, string> = {
low: "Low",
medium: "Medium",
high: "High",
xhigh: "Extra High",
};

return (
<Menu>
<MenuTrigger
Expand Down Expand Up @@ -72,8 +65,8 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls
>
{props.reasoningOptions.map((effort) => (
<MenuRadioItem key={effort} value={effort}>
{reasoningLabelByOption[effort]}
{effort === defaultReasoningEffort ? " (default)" : ""}
{CODEX_REASONING_EFFORT_LABELS[effort]}
{effort === props.defaultEffort ? " (default)" : ""}
</MenuRadioItem>
))}
</MenuRadioGroup>
Expand Down
39 changes: 38 additions & 1 deletion apps/web/src/composerDraftStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,11 +355,19 @@ describe("composerDraftStore codex fast mode", () => {
expect(useComposerDraftStore.getState().draftsByThreadId[threadId]?.codexFastMode).toBe(true);
});

it("clears codex fast mode when reset to the default", () => {
it("stores explicit fast mode off in the draft", () => {
const store = useComposerDraftStore.getState();
store.setCodexFastMode(threadId, true);
store.setCodexFastMode(threadId, false);

expect(useComposerDraftStore.getState().draftsByThreadId[threadId]?.codexFastMode).toBe(false);
});

it("clears codex fast mode when reset to follow the global preference", () => {
const store = useComposerDraftStore.getState();
store.setCodexFastMode(threadId, true);
store.setCodexFastMode(threadId, null);

expect(useComposerDraftStore.getState().draftsByThreadId[threadId]).toBeUndefined();
});
});
Expand Down Expand Up @@ -458,6 +466,35 @@ describe("composerDraftStore runtime and interaction settings", () => {
});
});

describe("composerDraftStore effort settings", () => {
const threadId = ThreadId.makeUnsafe("thread-effort");

beforeEach(() => {
useComposerDraftStore.setState({
draftsByThreadId: {},
draftThreadsByThreadId: {},
projectDraftThreadIdByProjectId: {},
});
});

it("stores explicit reasoning levels including high", () => {
const store = useComposerDraftStore.getState();

store.setEffort(threadId, "high");

expect(useComposerDraftStore.getState().draftsByThreadId[threadId]?.effort).toBe("high");
});

it("removes settings-only drafts when effort is cleared", () => {
const store = useComposerDraftStore.getState();

store.setEffort(threadId, "medium");
store.setEffort(threadId, null);

expect(useComposerDraftStore.getState().draftsByThreadId[threadId]).toBeUndefined();
});
});

// ---------------------------------------------------------------------------
// createDebouncedStorage
// ---------------------------------------------------------------------------
Expand Down
Loading
Loading