Skip to content
Closed
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/server/src/provider/CodexDeveloperInstructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ ${browserToolInstructions(browserToolsAvailable)}
export interface CodexRuntimeInfo {
readonly model: string;
readonly reasoningEffort: string;
readonly multiRepo?: boolean | undefined;
}

export function buildCodexDeveloperInstructions(
Expand Down
9 changes: 8 additions & 1 deletion apps/server/src/provider/Layers/AntigravityAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ interface TurnIntent {
interface SessionContext {
readonly threadId: ThreadId;
readonly cwd: string;
/** The session spans several repository roots. */
readonly multiRepo: boolean;
readonly nativeSessionId: string;
readonly scope: Scope.Closeable;
readonly runtime: Runtime;
Expand Down Expand Up @@ -865,6 +867,7 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
context = {
threadId: input.threadId,
cwd,
multiRepo: (input.additionalRoots?.length ?? 0) > 0,
nativeSessionId: started.sessionId,
scope: sessionScope,
runtime,
Expand Down Expand Up @@ -1085,7 +1088,11 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
...prompt,
{
type: "text",
text: buildRuntimeInstructions({ harness: "Antigravity", model }),
text: buildRuntimeInstructions({
harness: "Antigravity",
model,
multiRepo: context.multiRepo,
}),
},
],
},
Expand Down
23 changes: 23 additions & 0 deletions apps/server/src/provider/Layers/ClaudeAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,29 @@ describe("ClaudeAdapterLive", () => {
);
});

it.effect("asks a multi-repo session for absolute file paths", () => {
const harness = makeHarness();
return Effect.gen(function* () {
const adapter = yield* ClaudeAdapter;
yield* adapter.startSession({
threadId: THREAD_ID,
provider: ProviderDriverKind.make("claudeAgent"),
cwd: "/workspace",
additionalRoots: ["/workspace/api", "/workspace/web"],
runtimeMode: "full-access",
});

assert.deepEqual(harness.getLastCreateQueryInput()?.options.systemPrompt, {
type: "preset",
preset: "claude_code",
append: buildRuntimeInstructions({ harness: "Claude Code", multiRepo: true }),
});
}).pipe(
Effect.provideService(Random.Random, makeDeterministicRandomService()),
Effect.provide(harness.layer),
);
});

it.effect("derives auto permission mode from auto runtime policy without skip flag", () => {
const harness = makeHarness();
return Effect.gen(function* () {
Expand Down
5 changes: 4 additions & 1 deletion apps/server/src/provider/Layers/ClaudeAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4925,7 +4925,10 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
type: "preset",
preset: "claude_code",
// Model and effort can change after this session-level prompt is set.
append: buildRuntimeInstructions({ harness: "Claude Code" }),
append: buildRuntimeInstructions({
harness: "Claude Code",
multiRepo: (input.additionalRoots?.length ?? 0) > 0,
}),
},
settingSources: [...CLAUDE_SETTING_SOURCES],
// `ultracode` is a Claude Code setting, not an API effort level. It is
Expand Down
18 changes: 18 additions & 0 deletions apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,24 @@ describe("buildTurnStartParams", () => {
});
});

it.effect("asks a multi-repo session for absolute file paths", () =>
Effect.gen(function* () {
const instructions = function* (multiRepo: boolean) {
const params = yield* buildTurnStartParams({
threadId: "provider-thread-1",
runtimeMode: "full-access",
prompt: "Go",
interactionMode: "default",
multiRepo,
});
return params.collaborationMode?.settings.developer_instructions ?? "";
};

NodeAssert.match(yield* instructions(true), /<multi_repo_workspace>/);
NodeAssert.doesNotMatch(yield* instructions(false), /<multi_repo_workspace>/);
}),
);

it("reports the same fallback model and effort in settings and instructions", () => {
const params = Effect.runSync(
buildTurnStartParams({
Expand Down
7 changes: 6 additions & 1 deletion apps/server/src/provider/Layers/CodexSessionRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ function buildCodexCollaborationMode(input: {
readonly model?: string;
readonly effort?: EffectCodexSchema.V2TurnStartParams__ReasoningEffort;
readonly browserToolsAvailable?: boolean | T3CodeToolAvailability;
readonly multiRepo?: boolean;
}): EffectCodexSchema.V2TurnStartParams__CollaborationMode | undefined {
if (input.interactionMode === undefined) {
return undefined;
Expand All @@ -603,7 +604,7 @@ function buildCodexCollaborationMode(input: {
reasoning_effort: reasoningEffort,
developer_instructions: buildCodexDeveloperInstructions(
input.interactionMode,
{ model, reasoningEffort },
{ model, reasoningEffort, multiRepo: input.multiRepo },
input.browserToolsAvailable ?? true,
),
},
Expand All @@ -628,6 +629,8 @@ export function buildTurnStartParams(input: {
readonly interactionMode?: ProviderInteractionMode;
/** Defaults to true so callers that predate the agent-access gate are unchanged. */
readonly browserToolsAvailable?: boolean | T3CodeToolAvailability;
/** The session spans several repository roots. */
readonly multiRepo?: boolean;
}): Effect.Effect<
CodexTurnStartParamsWithCollaborationMode,
CodexErrors.CodexAppServerProtocolParseError
Expand All @@ -649,6 +652,7 @@ export function buildTurnStartParams(input: {
...(input.model ? { model: input.model } : {}),
...(input.effort ? { effort: input.effort } : {}),
browserToolsAvailable: input.browserToolsAvailable ?? true,
...(input.multiRepo ? { multiRepo: true } : {}),
});

return decodeCodexTurnStartParamsWithCollaborationMode({
Expand Down Expand Up @@ -2552,6 +2556,7 @@ export const makeCodexSessionRuntime = (
options.appServerArgs,
options.mcpCapabilities,
),
multiRepo: (options.additionalRoots?.length ?? 0) > 0,
});
const rawResponse = yield* client.raw.request("turn/start", params);
const response = yield* decodeV2TurnStartResponse(rawResponse).pipe(
Expand Down
9 changes: 8 additions & 1 deletion apps/server/src/provider/Layers/CursorAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ interface CursorSessionContext {
session: ProviderSession;
readonly scope: Scope.Closeable;
readonly acp: AcpSessionRuntime.AcpSessionRuntime["Service"];
/** The session spans several repository roots. */
readonly multiRepo: boolean;
notificationFiber: Fiber.Fiber<void, never> | undefined;
readonly pendingApprovals: Map<ApprovalRequestId, PendingApproval>;
readonly pendingUserInputs: Map<ApprovalRequestId, PendingUserInput>;
Expand Down Expand Up @@ -793,6 +795,7 @@ export function makeCursorAdapter(
session,
scope: sessionScope,
acp,
multiRepo: (input.additionalRoots?.length ?? 0) > 0,
notificationFiber: undefined,
pendingApprovals,
pendingUserInputs,
Expand Down Expand Up @@ -1097,7 +1100,11 @@ export function makeCursorAdapter(
...promptParts,
{
type: "text",
text: buildRuntimeInstructions({ harness: "Cursor", model: resolvedModel }),
text: buildRuntimeInstructions({
harness: "Cursor",
model: resolvedModel,
multiRepo: ctx.multiRepo,
}),
},
],
})
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/provider/Layers/GrokAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ interface GrokSessionContext {
session: ProviderSession;
readonly scope: Scope.Closeable;
readonly acp: AcpSessionRuntime.AcpSessionRuntime["Service"];
/** The session spans several repository roots. */
readonly multiRepo: boolean;
notificationFiber: Fiber.Fiber<void, never> | undefined;
readonly pendingApprovals: Map<ApprovalRequestId, PendingApproval>;
readonly pendingUserInputs: Map<ApprovalRequestId, PendingUserInput>;
Expand Down Expand Up @@ -1291,6 +1293,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
session,
scope: sessionScope,
acp,
multiRepo: (input.additionalRoots?.length ?? 0) > 0,
notificationFiber: undefined,
pendingApprovals,
pendingUserInputs,
Expand Down Expand Up @@ -1647,6 +1650,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte
harness: "Grok",
model: displayModel,
reasoningEffort: normalizeGrokReasoningEffort(requestedTurnReasoningEffort),
multiRepo: ctx.multiRepo,
});
for (let yieldAttempt = 0; yieldAttempt < 8; yieldAttempt += 1) {
yield* Effect.yieldNow;
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/provider/Layers/OpenCodeAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,8 @@ interface OpenCodeSessionContext {
readonly client: OpencodeClient;
readonly server: OpenCodeServerConnection;
readonly directory: string;
/** The session spans several repository roots. */
readonly multiRepo: boolean;
openCodeSessionId: string;
readonly relatedSessionIds: Set<string>;
readonly resolvedRequestIds: Set<string>;
Expand Down Expand Up @@ -3007,6 +3009,7 @@ export function makeOpenCodeAdapter(
client: started.client,
server: started.server,
directory,
multiRepo: (input.additionalRoots?.length ?? 0) > 0,
openCodeSessionId: started.openCodeSession.id,
relatedSessionIds: new Set([started.openCodeSession.id]),
resolvedRequestIds: new Set(),
Expand Down Expand Up @@ -3283,6 +3286,7 @@ export function makeOpenCodeAdapter(
system: buildRuntimeInstructions({
harness: "OpenCode",
model: `${parsedModel.providerID}/${parsedModel.modelID}`,
multiRepo: context.multiRepo,
}),
parts: [...(text ? [{ type: "text" as const, text }] : []), ...fileParts],
},
Expand Down
7 changes: 7 additions & 0 deletions apps/server/src/provider/RuntimeInstructions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,11 @@ describe("buildRuntimeInstructions", () => {
expect(instructions).toContain("through the Cursor harness.");
expect(instructions).not.toContain("reasoning effort");
});

it("asks for absolute file paths only when the session spans several repos", () => {
expect(buildRuntimeInstructions({ harness: "Codex", multiRepo: true })).toContain(
"<multi_repo_workspace>",
);
expect(buildRuntimeInstructions({ harness: "Codex" })).not.toContain("<multi_repo_workspace>");
});
});
8 changes: 7 additions & 1 deletion apps/server/src/provider/RuntimeInstructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@ const PULL_REQUEST_LINKING_INSTRUCTIONS = `<pull_request_linking>
When the t3-code MCP server exposes link_pull_request, you must use it to register every pull request you create or work on for this thread. Call link_pull_request with the full PR URL immediately after creating a PR or starting work on an existing PR. For a stack, call it for every layer, not just the current branch or the top PR. This applies when creating or updating PRs through gh, gh stack, another CLI, or the host API: those operations do not register the PRs with this thread. Linking an already-linked PR is safe. Before finishing PR work, call list_thread_pull_requests and link any PR from your work that is missing. Do not link unrelated PRs mentioned only as background. If a linking call fails, report that failure instead of claiming the PR is linked.
</pull_request_linking>`;

const MULTI_REPO_FILE_PATH_INSTRUCTIONS = `<multi_repo_workspace>
This workspace contains more than one git repository. Tools such as git print paths relative to the repository they ran in, so the same relative path can refer to files in different repositories. When you mention a file or directory in your response, write its absolute path.
</multi_repo_workspace>`;

/** Shared runtime context; omit model and effort when the harness manages them dynamically. */
export function buildRuntimeInstructions(runtime: {
readonly harness: string;
readonly model?: string | undefined;
readonly reasoningEffort?: string | undefined;
/** True when the session spans more than one repository root. */
readonly multiRepo?: boolean | undefined;
}): string {
const harness = toSingleLine(runtime.harness);
const model = toSingleLine(runtime.model ?? "");
const effort = toSingleLine(runtime.reasoningEffort ?? "");
const modelInfo = model && model !== "auto" && model !== "default" ? `, as ${model}` : "";
const effortInfo = effort ? ` with ${effort} reasoning effort` : "";
return `<runtime_info>In case you're asked: you are running in T3 Code through the ${harness} harness${modelInfo}${effortInfo}. No need to mention this otherwise. You can embed images and videos in your response using Markdown with absolute file paths.</runtime_info>\n\n${PULL_REQUEST_LINKING_INSTRUCTIONS}`;
return `<runtime_info>In case you're asked: you are running in T3 Code through the ${harness} harness${modelInfo}${effortInfo}. No need to mention this otherwise. You can embed images and videos in your response using Markdown with absolute file paths.</runtime_info>\n\n${PULL_REQUEST_LINKING_INSTRUCTIONS}${runtime.multiRepo ? `\n\n${MULTI_REPO_FILE_PATH_INSTRUCTIONS}` : ""}`;
}

function toSingleLine(value: string): string {
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/components/ChatMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2383,16 +2383,17 @@ function useChatMarkdownState({
return metaByHref;
}, [cwd, imageBaseDir, repoRootsKey, text]);
const inlineCodeFileLinkMetaByText = useMemo(() => {
const roots = repoRootsKey ? repoRootsKey.split("\0") : undefined;
const metaByText = new Map<string, MarkdownFileLinkMeta>();
for (const span of extractInlineCodeSpans(text)) {
if (metaByText.has(span)) continue;
const meta = resolveInlineCodeFileLinkMeta(span, cwd, imageBaseDir ?? cwd);
const meta = resolveInlineCodeFileLinkMeta(span, cwd, imageBaseDir ?? cwd, roots);
if (meta) {
metaByText.set(span, meta);
}
}
return metaByText;
}, [cwd, imageBaseDir, text]);
}, [cwd, imageBaseDir, repoRootsKey, text]);
const fileLinkParentSuffixByPath = useMemo(() => {
const filePaths = [
...[...markdownFileLinkMetaByHref.values()].map((meta) => meta.filePath),
Expand Down
Loading