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
3 changes: 3 additions & 0 deletions apps/server/src/serverSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,9 +500,11 @@ it.layer(NodeServices.layer)("server settings", (it) => {
},
},
automaticGitFetchInterval: Duration.seconds(10),
defaultRuntimeMode: "approval-required",
});

assert.equal(next.providers.codex.binaryPath, "/opt/homebrew/bin/codex");
assert.equal(next.defaultRuntimeMode, "approval-required");

const raw = yield* fileSystem.readFileString(serverConfig.settingsPath);
// @effect-diagnostics-next-line preferSchemaOverJson:off
Expand All @@ -522,6 +524,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
},
},
automaticGitFetchInterval: 10_000,
defaultRuntimeMode: "approval-required",
});
}).pipe(Effect.provide(makeServerSettingsLayer())),
);
Expand Down
13 changes: 8 additions & 5 deletions apps/web/src/components/ChatView.tsx
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,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 @@ -153,7 +152,10 @@ import { getProviderModelCapabilities, resolveSelectableProvider } from "../prov
import { useEnvironmentSettings } from "../hooks/useSettings";
import { resolveAppModelSelectionForInstance } from "../modelSelection";
import { getTerminalFocusOwner } from "../lib/terminalFocus";
import { resolveNewDraftStartFromOrigin } from "../lib/chatThreadActions";
import {
buildNewDraftExecutionDefaults,
resolveNewDraftStartFromOrigin,
} from "../lib/chatThreadActions";
import {
deriveLogicalProjectKeyFromSettings,
selectProjectGroupingSettings,
Expand Down Expand Up @@ -1242,7 +1244,8 @@ function ChatViewContent(props: ChatViewProps) {
const threadError = isServerThread
? (localServerError ?? serverThread?.session?.lastError ?? null)
: localDraftError;
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 @@ -1564,8 +1567,7 @@ function ChatViewContent(props: ChatViewProps) {
setLogicalProjectDraftThreadId(logicalProjectKey, activeProjectRef, nextDraftId, {
threadId: nextThreadId,
createdAt: new Date().toISOString(),
runtimeMode: DEFAULT_RUNTIME_MODE,
interactionMode: DEFAULT_INTERACTION_MODE,
...buildNewDraftExecutionDefaults(settings.defaultRuntimeMode),
...input,
});
await navigate({
Expand All @@ -1585,6 +1587,7 @@ function ChatViewContent(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/environment";
Expand Down Expand Up @@ -110,6 +111,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 @@ -412,6 +419,9 @@ export function useSettingsRestore(onRestored?: () => void) {
DEFAULT_UNIFIED_SETTINGS.newWorktreesStartFromOrigin
? ["New worktrees start from origin"]
: []),
...(settings.defaultRuntimeMode !== DEFAULT_UNIFIED_SETTINGS.defaultRuntimeMode
? ["Default access mode"]
: []),
...(settings.addProjectBaseDirectory !== DEFAULT_UNIFIED_SETTINGS.addProjectBaseDirectory
? ["Add project base directory"]
: []),
Expand All @@ -429,6 +439,7 @@ export function useSettingsRestore(onRestored?: () => void) {
settings.confirmThreadArchive,
settings.confirmThreadDelete,
settings.addProjectBaseDirectory,
settings.defaultRuntimeMode,
settings.defaultThreadEnvMode,
settings.newWorktreesStartFromOrigin,
settings.diffIgnoreWhitespace,
Expand Down Expand Up @@ -462,6 +473,7 @@ export function useSettingsRestore(onRestored?: () => void) {
automaticGitFetchInterval: DEFAULT_UNIFIED_SETTINGS.automaticGitFetchInterval,
defaultThreadEnvMode: DEFAULT_UNIFIED_SETTINGS.defaultThreadEnvMode,
newWorktreesStartFromOrigin: DEFAULT_UNIFIED_SETTINGS.newWorktreesStartFromOrigin,
defaultRuntimeMode: DEFAULT_UNIFIED_SETTINGS.defaultRuntimeMode,
addProjectBaseDirectory: DEFAULT_UNIFIED_SETTINGS.addProjectBaseDirectory,
confirmThreadArchive: DEFAULT_UNIFIED_SETTINGS.confirmThreadArchive,
confirmThreadDelete: DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete,
Expand Down Expand Up @@ -797,6 +809,52 @@ export function GeneralSettingsPanel() {
/>
) : null}

<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
13 changes: 6 additions & 7 deletions apps/web/src/hooks/useHandleNewThread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@ import {
scopeProjectRef,
scopeThreadRef,
} from "@t3tools/client-runtime/environment";
import {
DEFAULT_RUNTIME_MODE,
DEFAULT_SERVER_SETTINGS,
type ScopedProjectRef,
} from "@t3tools/contracts";
import { DEFAULT_SERVER_SETTINGS, type ScopedProjectRef } from "@t3tools/contracts";
import { useParams, useRouter } from "@tanstack/react-router";
import { useCallback, useMemo } from "react";
import {
Expand All @@ -24,7 +20,10 @@ import {
selectProjectGroupingSettings,
} from "../logicalProject";
import { readThreadShell, useProjects, useServerConfigs, useThread } from "../state/entities";
import { resolveNewDraftStartFromOrigin } from "../lib/chatThreadActions";
import {
buildNewDraftExecutionDefaults,
resolveNewDraftStartFromOrigin,
} from "../lib/chatThreadActions";
import { resolveThreadRouteTarget } from "../threadRoutes";
import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore";
import { useClientSettings } from "./useSettings";
Expand Down Expand Up @@ -173,7 +172,7 @@ export function useNewThreadHandler() {
envMode: initialEnvMode,
newWorktreesStartFromOrigin: environmentSettings.newWorktreesStartFromOrigin,
}),
runtimeMode: DEFAULT_RUNTIME_MODE,
...buildNewDraftExecutionDefaults(environmentSettings.defaultRuntimeMode),
});
applyStickyState(draftId);

Expand Down
10 changes: 9 additions & 1 deletion apps/web/src/lib/chatThreadActions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { scopeProjectRef } from "@t3tools/client-runtime/environment";
import { EnvironmentId, ProjectId } from "@t3tools/contracts";
import { describe, expect, it, vi } from "vite-plus/test";
import {
resolveThreadActionProjectRef,
buildNewDraftExecutionDefaults,
resolveNewDraftStartFromOrigin,
resolveThreadActionProjectRef,
startNewLocalThreadFromContext,
startNewThreadFromContext,
type ChatThreadActionContext,
Expand All @@ -24,6 +25,13 @@ function createContext(overrides: Partial<ChatThreadActionContext> = {}): ChatTh
}

describe("chatThreadActions", () => {
it("initializes new drafts with the configured default access mode", () => {
expect(buildNewDraftExecutionDefaults("approval-required")).toEqual({
runtimeMode: "approval-required",
interactionMode: "default",
});
});

it("only applies the start-from-origin default to new worktree drafts", () => {
expect(
resolveNewDraftStartFromOrigin({
Expand Down
19 changes: 18 additions & 1 deletion apps/web/src/lib/chatThreadActions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { scopeProjectRef } from "@t3tools/client-runtime/environment";
import type { EnvironmentId, ProjectId, ScopedProjectRef } from "@t3tools/contracts";
import {
DEFAULT_PROVIDER_INTERACTION_MODE,
type EnvironmentId,
type ProjectId,
type ProviderInteractionMode,
type RuntimeMode,
type ScopedProjectRef,
} from "@t3tools/contracts";
import type { DraftThreadEnvMode } from "../composerDraftStore";

interface ThreadContextLike {
Expand Down Expand Up @@ -42,6 +49,16 @@ export function resolveNewDraftStartFromOrigin(input: {
return input.envMode === "worktree" && input.newWorktreesStartFromOrigin;
}

export function buildNewDraftExecutionDefaults(defaultRuntimeMode: RuntimeMode): {
runtimeMode: RuntimeMode;
interactionMode: ProviderInteractionMode;
} {
return {
runtimeMode: defaultRuntimeMode,
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
};
}

export function resolveThreadActionProjectRef(
context: ChatThreadActionContext,
): ScopedProjectRef | null {
Expand Down
14 changes: 14 additions & 0 deletions packages/contracts/src/settings.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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 {
ClientSettingsSchema,
Expand All @@ -14,6 +15,19 @@ const decodeServerSettings = Schema.decodeUnknownSync(ServerSettings);
const decodeServerSettingsPatch = Schema.decodeUnknownSync(ServerSettingsPatch);
const encodeServerSettings = Schema.encodeSync(ServerSettings);

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

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

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

describe("ClientSettings word wrap", () => {
it("defaults word wrap on", () => {
expect(decodeClientSettings({}).wordWrap).toBe(true);
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 @@ -374,6 +374,9 @@ export const ServerSettings = Schema.Struct({
defaultThreadEnvMode: ThreadEnvMode.pipe(
Schema.withDecodingDefault(Effect.succeed("local" as const satisfies ThreadEnvMode)),
),
defaultRuntimeMode: RuntimeMode.pipe(
Schema.withDecodingDefault(Effect.succeed(DEFAULT_RUNTIME_MODE)),
),
newWorktreesStartFromOrigin: Schema.Boolean.pipe(
Schema.withDecodingDefault(Effect.succeed(false)),
),
Expand Down Expand Up @@ -507,6 +510,7 @@ export const ServerSettingsPatch = Schema.Struct({
enableProviderUpdateChecks: Schema.optionalKey(Schema.Boolean),
automaticGitFetchInterval: Schema.optionalKey(Schema.DurationFromMillis),
defaultThreadEnvMode: Schema.optionalKey(ThreadEnvMode),
defaultRuntimeMode: Schema.optionalKey(RuntimeMode),
newWorktreesStartFromOrigin: Schema.optionalKey(Schema.Boolean),
addProjectBaseDirectory: Schema.optionalKey(TrimmedString),
textGenerationModelSelection: Schema.optionalKey(ModelSelectionPatch),
Expand Down
Loading