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
31 changes: 27 additions & 4 deletions apps/server/src/provider/CodexDeveloperInstructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@ For browser work, first call \`preview_status\`. If no automation-capable previe
Do not switch to global browser skills, Chrome, Node REPL browser automation, standalone Playwright, or agent-browser merely because the preview is initially closed or a first call fails. Use an alternative browser system only when the T3 preview tools are absent, the user explicitly requests another browser, or \`preview_open\` returns an explicit unsupported/unavailable error. A failed T3 preview tool call should be inspected and retried with corrected arguments when the error is actionable.
`;

export const CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS = `<collaboration_mode># Plan Mode (Conversational)
const withBrowserToolInstructions = (instructions: string, enabled: boolean): string =>
enabled
? instructions.replace(
"\n</collaboration_mode>",
`${T3_CODE_BROWSER_TOOL_INSTRUCTIONS}\n</collaboration_mode>`,
)
: instructions;

const CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS_BASE = `<collaboration_mode># Plan Mode (Conversational)

You work in 3 phases, and you should *chat your way* to a great plan before finalizing it. A great plan is very detailed-intent- and implementation-wise-so that it can be handed to another engineer or agent to be implemented right away. It must be **decision complete**, where the implementer does not need to make any decisions.

Expand Down Expand Up @@ -129,10 +137,9 @@ plan content should be human and agent digestible. The final plan must be plan-o
Do not ask "should I proceed?" in the final output. The user can easily switch out of Plan mode and request implementation if you have included a \`<proposed_plan>\` block in your response. Alternatively, they can decide to stay in Plan mode and continue refining the plan.

Only produce at most one \`<proposed_plan>\` block per turn, and only when you are presenting a complete spec.
${T3_CODE_BROWSER_TOOL_INSTRUCTIONS}
</collaboration_mode>`;

export const CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS = `<collaboration_mode># Collaboration Mode: Default
const CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS_BASE = `<collaboration_mode># Collaboration Mode: Default

You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active.

Expand All @@ -143,5 +150,21 @@ Your active mode changes only when new developer instructions with a different \
The \`request_user_input\` tool is unavailable in Default mode. If you call it while in Default mode, it will return an error.

In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, ask the user directly with a concise plain-text question. Never write a multiple choice question as a textual assistant message.
${T3_CODE_BROWSER_TOOL_INSTRUCTIONS}
</collaboration_mode>`;

export const buildCodexDeveloperInstructions = (
mode: "default" | "plan",
browserEnabled: boolean,
): string =>
withBrowserToolInstructions(
mode === "plan"
? CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS_BASE
: CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS_BASE,
browserEnabled,
);

export const CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS = buildCodexDeveloperInstructions("plan", true);
export const CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS = buildCodexDeveloperInstructions(
"default",
true,
);
10 changes: 10 additions & 0 deletions apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as CodexErrors from "effect-codex-app-server/errors";
import * as CodexRpc from "effect-codex-app-server/rpc";

import {
buildCodexDeveloperInstructions,
CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS,
CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS,
} from "../CodexDeveloperInstructions.ts";
Expand Down Expand Up @@ -206,6 +207,15 @@ describe("T3 browser developer instructions", () => {
NodeAssert.match(instructions, /Do not switch to global browser skills/);
}
});

it("omits browser instructions when browser access is disabled", () => {
for (const mode of ["default", "plan"] as const) {
const instructions = buildCodexDeveloperInstructions(mode, false);
NodeAssert.doesNotMatch(instructions, /t3-code/);
NodeAssert.doesNotMatch(instructions, /preview_status/);
NodeAssert.doesNotMatch(instructions, /preview_open/);
}
});
});

describe("hasConfiguredMcpServer", () => {
Expand Down
17 changes: 9 additions & 8 deletions apps/server/src/provider/Layers/CodexSessionRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,7 @@ import * as EffectCodexSchema from "effect-codex-app-server/schema";

import { buildCodexInitializeParams } from "./CodexProvider.ts";
import { expandHomePath } from "../../pathExpansion.ts";
import {
CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS,
CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS,
} from "../CodexDeveloperInstructions.ts";
import { buildCodexDeveloperInstructions } from "../CodexDeveloperInstructions.ts";
const decodeV2TurnStartResponse = Schema.decodeUnknownEffect(EffectCodexSchema.V2TurnStartResponse);

const PROVIDER = ProviderDriverKind.make("codex");
Expand Down Expand Up @@ -326,6 +323,7 @@ function buildCodexCollaborationMode(input: {
readonly interactionMode?: ProviderInteractionMode;
readonly model?: string;
readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort;
readonly browserEnabled?: boolean;
}): EffectCodexSchema.V2TurnStartParams__CollaborationMode | undefined {
if (input.interactionMode === undefined) {
return undefined;
Expand All @@ -336,10 +334,10 @@ function buildCodexCollaborationMode(input: {
settings: {
model,
reasoning_effort: input.effort ?? "medium",
developer_instructions:
input.interactionMode === "plan"
? CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS
: CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS,
developer_instructions: buildCodexDeveloperInstructions(
input.interactionMode,
input.browserEnabled ?? true,
),
},
};
}
Expand All @@ -356,6 +354,7 @@ export function buildTurnStartParams(input: {
readonly serviceTier?: CodexServiceTier;
readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort;
readonly interactionMode?: ProviderInteractionMode;
readonly browserEnabled?: boolean;
}): Effect.Effect<
CodexTurnStartParamsWithCollaborationMode,
CodexErrors.CodexAppServerProtocolParseError
Expand All @@ -376,6 +375,7 @@ export function buildTurnStartParams(input: {
...(input.interactionMode ? { interactionMode: input.interactionMode } : {}),
...(input.model ? { model: input.model } : {}),
...(input.effort ? { effort: input.effort } : {}),
...(input.browserEnabled !== undefined ? { browserEnabled: input.browserEnabled } : {}),
});

return decodeCodexTurnStartParamsWithCollaborationMode({
Expand Down Expand Up @@ -1286,6 +1286,7 @@ export const makeCodexSessionRuntime = (
...(input.serviceTier ? { serviceTier: input.serviceTier } : {}),
...(input.effort ? { effort: input.effort } : {}),
...(input.interactionMode ? { interactionMode: input.interactionMode } : {}),
browserEnabled: hasConfiguredMcpServer(options.appServerArgs),
});
const rawResponse = yield* client.raw.request("turn/start", params);
const response = yield* decodeV2TurnStartResponse(rawResponse).pipe(
Expand Down
29 changes: 21 additions & 8 deletions apps/server/src/provider/Layers/ProviderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import * as ProviderEventLoggers from "./ProviderEventLoggers.ts";
import * as AnalyticsService from "../../telemetry/AnalyticsService.ts";
import * as McpProviderSession from "../../mcp/McpProviderSession.ts";
import * as McpSessionRegistry from "../../mcp/McpSessionRegistry.ts";
import * as ServerSettings from "../../serverSettings.ts";
const isModelSelection = Schema.is(ModelSelection);

/**
Expand Down Expand Up @@ -212,16 +213,28 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (

const registry = yield* ProviderAdapterRegistry.ProviderAdapterRegistry;
const directory = yield* ProviderSessionDirectory.ProviderSessionDirectory;
const serverSettings = yield* ServerSettings.ServerSettingsService;
const runtimeEventPubSub = yield* PubSub.unbounded<ProviderRuntimeEvent>();
const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
const prepareMcpSession = (threadId: ThreadId, providerInstanceId: ProviderInstanceId) =>
McpSessionRegistry.issueActiveMcpCredential({ threadId, providerInstanceId }).pipe(
Effect.tap((credential) =>
credential
? Effect.sync(() => McpProviderSession.setMcpProviderSession(credential.config))
: Effect.void,
),
);
const prepareMcpSession = Effect.fn("ProviderService.prepareMcpSession")(function* (
threadId: ThreadId,
providerInstanceId: ProviderInstanceId,
) {
const settings = yield* serverSettings.getSettings.pipe(Effect.orDie);
if (!settings.enableBuiltInBrowser) {
yield* McpSessionRegistry.revokeActiveMcpThread(threadId);
McpProviderSession.clearMcpProviderSession(threadId);
return;
}

const credential = yield* McpSessionRegistry.issueActiveMcpCredential({
threadId,
providerInstanceId,
});
if (credential) {
McpProviderSession.setMcpProviderSession(credential.config);
}
});
const clearMcpSession = (threadId: ThreadId) =>
McpSessionRegistry.revokeActiveMcpThread(threadId).pipe(
Effect.tap(() => Effect.sync(() => McpProviderSession.clearMcpProviderSession(threadId))),
Expand Down
31 changes: 31 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,9 @@ export function useSettingsRestore(onRestored?: () => void) {
...(settings.enableAssistantStreaming !== DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming
? ["Assistant output"]
: []),
...(settings.enableBuiltInBrowser !== DEFAULT_UNIFIED_SETTINGS.enableBuiltInBrowser
? ["Built-in browser"]
: []),
...(Duration.toMillis(settings.automaticGitFetchInterval) !==
Duration.toMillis(DEFAULT_UNIFIED_SETTINGS.automaticGitFetchInterval)
? ["Automatic Git fetch interval"]
Expand Down Expand Up @@ -434,6 +437,7 @@ export function useSettingsRestore(onRestored?: () => void) {
settings.diffIgnoreWhitespace,
settings.automaticGitFetchInterval,
settings.enableAssistantStreaming,
settings.enableBuiltInBrowser,
settings.sidebarThreadPreviewCount,
settings.timestampFormat,
settings.wordWrap,
Expand All @@ -459,6 +463,7 @@ export function useSettingsRestore(onRestored?: () => void) {
sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount,
autoOpenPlanSidebar: DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar,
enableAssistantStreaming: DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming,
enableBuiltInBrowser: DEFAULT_UNIFIED_SETTINGS.enableBuiltInBrowser,
automaticGitFetchInterval: DEFAULT_UNIFIED_SETTINGS.automaticGitFetchInterval,
defaultThreadEnvMode: DEFAULT_UNIFIED_SETTINGS.defaultThreadEnvMode,
newWorktreesStartFromOrigin: DEFAULT_UNIFIED_SETTINGS.newWorktreesStartFromOrigin,
Expand Down Expand Up @@ -695,6 +700,32 @@ export function GeneralSettingsPanel() {
}
/>

<SettingsRow
title="Built-in browser"
description="Allow agents to use T3 Code's browser tools. Applies to new agent sessions."
resetAction={
settings.enableBuiltInBrowser !== DEFAULT_UNIFIED_SETTINGS.enableBuiltInBrowser ? (
<SettingResetButton
label="built-in browser"
onClick={() =>
updateSettings({
enableBuiltInBrowser: DEFAULT_UNIFIED_SETTINGS.enableBuiltInBrowser,
})
}
/>
) : null
}
control={
<Switch
checked={settings.enableBuiltInBrowser}
onCheckedChange={(checked) =>
updateSettings({ enableBuiltInBrowser: Boolean(checked) })
}
aria-label="Allow agents to use the built-in browser"
/>
}
/>

<SettingsRow
title="Auto-open task panel"
description="Open the right-side plan and task panel automatically when steps appear."
Expand Down
4 changes: 4 additions & 0 deletions packages/contracts/src/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ describe("ServerSettings.providerInstances (slice-2 invariant)", () => {
});

describe("ServerSettings worktree defaults", () => {
it("enables the built-in browser for legacy configs", () => {
expect(decodeServerSettings({}).enableBuiltInBrowser).toBe(true);
});

it("defaults start-from-origin off for legacy configs", () => {
expect(decodeServerSettings({}).newWorktreesStartFromOrigin).toBe(false);
});
Expand Down
2 changes: 2 additions & 0 deletions packages/contracts/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ export const DEFAULT_AUTOMATIC_GIT_FETCH_INTERVAL = Duration.seconds(30);

export const ServerSettings = Schema.Struct({
enableAssistantStreaming: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
enableBuiltInBrowser: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
enableProviderUpdateChecks: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
automaticGitFetchInterval: Schema.DurationFromMillis.pipe(
Schema.withDecodingDefault(
Expand Down Expand Up @@ -504,6 +505,7 @@ const OpenCodeSettingsPatch = Schema.Struct({
export const ServerSettingsPatch = Schema.Struct({
// Server settings
enableAssistantStreaming: Schema.optionalKey(Schema.Boolean),
enableBuiltInBrowser: Schema.optionalKey(Schema.Boolean),
enableProviderUpdateChecks: Schema.optionalKey(Schema.Boolean),
automaticGitFetchInterval: Schema.optionalKey(Schema.DurationFromMillis),
defaultThreadEnvMode: Schema.optionalKey(ThreadEnvMode),
Expand Down
Loading