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/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const clientSettings: ClientSettings = {
autoOpenPlanSidebar: false,
confirmThreadArchive: true,
confirmThreadDelete: false,
defaultRuntimeMode: "full-access",
dismissedProviderUpdateNotificationKeys: [],
diffIgnoreWhitespace: true,
diffWordWrap: true,
Expand Down
7 changes: 4 additions & 3 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ import {
} from "../proposedPlan";
import {
DEFAULT_INTERACTION_MODE,
DEFAULT_RUNTIME_MODE,
DEFAULT_THREAD_TERMINAL_ID,
MAX_TERMINALS_PER_GROUP,
type ChatMessage,
Expand Down Expand Up @@ -1227,7 +1226,8 @@ export default function ChatView(props: ChatViewProps) {
);
const isServerThread = routeKind === "server" && serverThread !== undefined;
const activeThread = isServerThread ? serverThread : localDraftThread;
const runtimeMode = composerRuntimeMode ?? activeThread?.runtimeMode ?? DEFAULT_RUNTIME_MODE;
const runtimeMode =
composerRuntimeMode ?? activeThread?.runtimeMode ?? settings.defaultRuntimeMode;
const interactionMode =
composerInteractionMode ?? activeThread?.interactionMode ?? DEFAULT_INTERACTION_MODE;
const isLocalDraftThread = !isServerThread && localDraftThread !== undefined;
Expand Down Expand Up @@ -1567,7 +1567,7 @@ export default function ChatView(props: ChatViewProps) {
setLogicalProjectDraftThreadId(logicalProjectKey, activeProjectRef, nextDraftId, {
threadId: nextThreadId,
createdAt: new Date().toISOString(),
runtimeMode: DEFAULT_RUNTIME_MODE,
runtimeMode: settings.defaultRuntimeMode,
interactionMode: DEFAULT_INTERACTION_MODE,
...input,
});
Expand All @@ -1588,6 +1588,7 @@ export default function ChatView(props: ChatViewProps) {
routeKind,
setDraftThreadContext,
setLogicalProjectDraftThreadId,
settings.defaultRuntimeMode,
],
);

Expand Down
58 changes: 58 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ProviderDriverKind,
type ProviderInstanceConfig,
type ProviderInstanceId,
type RuntimeMode,
type ScopedThreadRef,
} from "@t3tools/contracts";
import { scopeThreadRef } from "@t3tools/client-runtime";
Expand Down Expand Up @@ -101,6 +102,12 @@ const TIMESTAMP_FORMAT_LABELS = {
"24-hour": "24-hour",
} as const;

const RUNTIME_MODE_LABELS: Record<RuntimeMode, string> = {
"approval-required": "Supervised",
"auto-accept-edits": "Auto-accept edits",
"full-access": "Full access",
};

const DEFAULT_DRIVER_KIND = ProviderDriverKind.make("codex");

function withoutProviderInstanceKey<V>(
Expand Down Expand Up @@ -417,6 +424,9 @@ export function useSettingsRestore(onRestored?: () => void) {
...(settings.defaultThreadEnvMode !== DEFAULT_UNIFIED_SETTINGS.defaultThreadEnvMode
? ["New thread mode"]
: []),
...(settings.defaultRuntimeMode !== DEFAULT_UNIFIED_SETTINGS.defaultRuntimeMode
? ["Default access mode"]
: []),
...(settings.addProjectBaseDirectory !== DEFAULT_UNIFIED_SETTINGS.addProjectBaseDirectory
? ["Add project base directory"]
: []),
Expand All @@ -434,6 +444,7 @@ export function useSettingsRestore(onRestored?: () => void) {
settings.confirmThreadArchive,
settings.confirmThreadDelete,
settings.addProjectBaseDirectory,
settings.defaultRuntimeMode,
settings.defaultThreadEnvMode,
settings.diffIgnoreWhitespace,
settings.diffWordWrap,
Expand Down Expand Up @@ -465,6 +476,7 @@ export function useSettingsRestore(onRestored?: () => void) {
enableAssistantStreaming: DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming,
automaticGitFetchInterval: DEFAULT_UNIFIED_SETTINGS.automaticGitFetchInterval,
defaultThreadEnvMode: DEFAULT_UNIFIED_SETTINGS.defaultThreadEnvMode,
defaultRuntimeMode: DEFAULT_UNIFIED_SETTINGS.defaultRuntimeMode,
addProjectBaseDirectory: DEFAULT_UNIFIED_SETTINGS.addProjectBaseDirectory,
confirmThreadArchive: DEFAULT_UNIFIED_SETTINGS.confirmThreadArchive,
confirmThreadDelete: DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete,
Expand Down Expand Up @@ -738,6 +750,52 @@ export function GeneralSettingsPanel() {
}
/>

<SettingsRow
title="Default access"
description="Pick the permission mode used when newly created draft threads start."
resetAction={
settings.defaultRuntimeMode !== DEFAULT_UNIFIED_SETTINGS.defaultRuntimeMode ? (
<SettingResetButton
label="default access mode"
onClick={() =>
updateSettings({
defaultRuntimeMode: DEFAULT_UNIFIED_SETTINGS.defaultRuntimeMode,
})
}
/>
) : null
}
control={
<Select
value={settings.defaultRuntimeMode}
onValueChange={(value) => {
if (
value === "approval-required" ||
value === "auto-accept-edits" ||
value === "full-access"
) {
updateSettings({ defaultRuntimeMode: value });
}
}}
>
<SelectTrigger className="w-full sm:w-44" aria-label="Default access mode">
<SelectValue>{RUNTIME_MODE_LABELS[settings.defaultRuntimeMode]}</SelectValue>
</SelectTrigger>
<SelectPopup align="end" alignItemWithTrigger={false}>
<SelectItem hideIndicator value="approval-required">
{RUNTIME_MODE_LABELS["approval-required"]}
</SelectItem>
<SelectItem hideIndicator value="auto-accept-edits">
{RUNTIME_MODE_LABELS["auto-accept-edits"]}
</SelectItem>
<SelectItem hideIndicator value="full-access">
{RUNTIME_MODE_LABELS["full-access"]}
</SelectItem>
</SelectPopup>
</Select>
}
/>

<SettingsRow
title="Add project starts in"
description='Leave empty to use "~/" when the Add Project browser opens.'
Expand Down
7 changes: 4 additions & 3 deletions apps/web/src/hooks/useHandleNewThread.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { scopedProjectKey, scopeProjectRef } from "@t3tools/client-runtime";
import { DEFAULT_RUNTIME_MODE, type ScopedProjectRef } from "@t3tools/contracts";
import { type ScopedProjectRef, type RuntimeMode } from "@t3tools/contracts";
import { useParams, useRouter } from "@tanstack/react-router";
import { useCallback, useMemo } from "react";
import { useShallow } from "zustand/react/shallow";
Expand All @@ -24,6 +24,7 @@ import { useSettings } from "./useSettings";
function useNewThreadState() {
const projects = useStore(useShallow((store) => selectProjectsAcrossEnvironments(store)));
const projectGroupingSettings = useSettings(selectProjectGroupingSettings);
const defaultRuntimeMode = useSettings<RuntimeMode>((settings) => settings.defaultRuntimeMode);
const router = useRouter();
const getCurrentRouteTarget = useCallback(() => {
const currentRouteParams = router.state.matches[router.state.matches.length - 1]?.params ?? {};
Expand Down Expand Up @@ -125,7 +126,7 @@ function useNewThreadState() {
branch: options?.branch ?? null,
worktreePath: options?.worktreePath ?? null,
envMode: options?.envMode ?? "local",
runtimeMode: DEFAULT_RUNTIME_MODE,
runtimeMode: defaultRuntimeMode,
});
applyStickyState(draftId);

Expand All @@ -135,7 +136,7 @@ function useNewThreadState() {
});
})();
},
[getCurrentRouteTarget, projectGroupingSettings, router, projects],
[defaultRuntimeMode, getCurrentRouteTarget, projectGroupingSettings, router, projects],
);
}

Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/localApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,7 @@ describe("wsApi", () => {
autoOpenPlanSidebar: false,
confirmThreadArchive: true,
confirmThreadDelete: false,
defaultRuntimeMode: "full-access" as const,
dismissedProviderUpdateNotificationKeys: [],
diffIgnoreWhitespace: true,
diffWordWrap: true,
Expand Down Expand Up @@ -726,6 +727,7 @@ describe("wsApi", () => {
autoOpenPlanSidebar: false,
confirmThreadArchive: true,
confirmThreadDelete: false,
defaultRuntimeMode: "full-access" as const,
dismissedProviderUpdateNotificationKeys: [],
diffIgnoreWhitespace: true,
diffWordWrap: true,
Expand Down
25 changes: 24 additions & 1 deletion packages/contracts/src/settings.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,36 @@
import { describe, expect, it } from "vite-plus/test";
import * as Schema from "effect/Schema";

import { DEFAULT_RUNTIME_MODE } from "./orchestration.ts";
import { ProviderInstanceId } from "./providerInstance.ts";
import { DEFAULT_SERVER_SETTINGS, ServerSettings, ServerSettingsPatch } from "./settings.ts";
import {
ClientSettingsPatch,
ClientSettingsSchema,
DEFAULT_CLIENT_SETTINGS,
DEFAULT_SERVER_SETTINGS,
ServerSettings,
ServerSettingsPatch,
} from "./settings.ts";

const decodeClientSettings = Schema.decodeUnknownSync(ClientSettingsSchema);
const decodeClientSettingsPatch = Schema.decodeUnknownSync(ClientSettingsPatch);
const decodeServerSettings = Schema.decodeUnknownSync(ServerSettings);
const decodeServerSettingsPatch = Schema.decodeUnknownSync(ServerSettingsPatch);
const encodeServerSettings = Schema.encodeSync(ServerSettings);

describe("ClientSettings default runtime mode", () => {
it("defaults new thread access mode to the existing runtime default", () => {
expect(DEFAULT_CLIENT_SETTINGS.defaultRuntimeMode).toBe(DEFAULT_RUNTIME_MODE);
expect(decodeClientSettings({}).defaultRuntimeMode).toBe(DEFAULT_RUNTIME_MODE);
});

it("accepts runtime mode patches", () => {
const patch = decodeClientSettingsPatch({ defaultRuntimeMode: "approval-required" });

expect(patch.defaultRuntimeMode).toBe("approval-required");
});
});

describe("ServerSettings.providerInstances (slice-2 invariant)", () => {
it("defaults to an empty record so legacy configs without the key still decode", () => {
expect(DEFAULT_SERVER_SETTINGS.providerInstances).toEqual({});
Expand Down
6 changes: 5 additions & 1 deletion packages/contracts/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as Schema from "effect/Schema";
import * as SchemaTransformation from "effect/SchemaTransformation";
import { TrimmedNonEmptyString, TrimmedString } from "./baseSchemas.ts";
import { DEFAULT_GIT_TEXT_GENERATION_MODEL, ProviderOptionSelections } from "./model.ts";
import { ModelSelection } from "./orchestration.ts";
import { DEFAULT_RUNTIME_MODE, ModelSelection, RuntimeMode } from "./orchestration.ts";
import { ProviderInstanceConfig, ProviderInstanceId } from "./providerInstance.ts";

// ── Client Settings (local-only) ───────────────────────────────
Expand Down Expand Up @@ -43,6 +43,9 @@ export const ClientSettingsSchema = Schema.Struct({
autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
confirmThreadArchive: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
confirmThreadDelete: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
defaultRuntimeMode: RuntimeMode.pipe(
Schema.withDecodingDefault(Effect.succeed(DEFAULT_RUNTIME_MODE)),
),
dismissedProviderUpdateNotificationKeys: Schema.Array(TrimmedNonEmptyString).pipe(
Schema.withDecodingDefault(Effect.succeed([])),
),
Expand Down Expand Up @@ -510,6 +513,7 @@ export const ClientSettingsPatch = Schema.Struct({
autoOpenPlanSidebar: Schema.optionalKey(Schema.Boolean),
confirmThreadArchive: Schema.optionalKey(Schema.Boolean),
confirmThreadDelete: Schema.optionalKey(Schema.Boolean),
defaultRuntimeMode: Schema.optionalKey(RuntimeMode),
diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean),
diffWordWrap: Schema.optionalKey(Schema.Boolean),
favorites: Schema.optionalKey(
Expand Down
Loading