| author | |
| committer | |
| log | 61a91b6ef1bd45424169c6650362b358d49bbe34 |
| tree | 041c3e9a41104f6010dec012fecbf8a6e92e925a |
| parent | 00f8b7c28056188e3c5630160806a0afe51c9010 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Code <noreply@anthropic.com>17 files changed, 239 insertions(+), 173 deletions(-)
apps/server/src/provider/CodexDeveloperInstructions.ts+2-6| ... | @@ -1,4 +1,5 @@ | ... | @@ -1,4 +1,5 @@ |
| 1 | import type { ProviderInteractionMode } from "@t3tools/contracts"; | 1 | import type { ProviderInteractionMode } from "@t3tools/contracts"; |
| 2 | import { buildRuntimeInstructions } from "./RuntimeInstructions.ts"; | ||
| 2 | 3 | ||
| 3 | const T3_CODE_BROWSER_TOOL_INSTRUCTIONS = ` | 4 | const T3_CODE_BROWSER_TOOL_INSTRUCTIONS = ` |
| 4 | 5 | ||
| ... | @@ -175,11 +176,6 @@ export interface CodexRuntimeInfo { | ... | @@ -175,11 +176,6 @@ export interface CodexRuntimeInfo { |
| 175 | readonly reasoningEffort: string; | 176 | readonly reasoningEffort: string; |
| 176 | } | 177 | } |
| 177 | 178 | ||
| 178 | // Values come from trusted config, but keep the block single-line regardless. | ||
| 179 | function toSingleLine(value: string): string { | ||
| 180 | return value.replaceAll(/\s+/g, " ").trim(); | ||
| 181 | } | ||
| 182 | |||
| 183 | export function buildCodexDeveloperInstructions( | 179 | export function buildCodexDeveloperInstructions( |
| 184 | interactionMode: ProviderInteractionMode, | 180 | interactionMode: ProviderInteractionMode, |
| 185 | runtime: CodexRuntimeInfo, | 181 | runtime: CodexRuntimeInfo, |
| ... | @@ -196,5 +192,5 @@ export function buildCodexDeveloperInstructions( | ... | @@ -196,5 +192,5 @@ export function buildCodexDeveloperInstructions( |
| 196 | : codexDefaultModeDeveloperInstructions(browserToolsAvailable); | 192 | : codexDefaultModeDeveloperInstructions(browserToolsAvailable); |
| 197 | return `${base} | 193 | return `${base} |
| 198 | 194 | ||
| 199 | <runtime_info>In case you're asked: you are running in T3 Code through the Codex harness, as ${toSingleLine(runtime.model)} with ${toSingleLine(runtime.reasoningEffort)} reasoning effort. No need to mention this otherwise.</runtime_info>`; | 195 | ${buildRuntimeInstructions({ harness: "Codex", ...runtime })}`; |
| 200 | } | 196 | } |
apps/server/src/provider/Layers/AntigravityAdapter.test.ts+15-2| ... | @@ -49,6 +49,7 @@ const decodeRequestLog = Schema.decodeEffect( | ... | @@ -49,6 +49,7 @@ const decodeRequestLog = Schema.decodeEffect( |
| 49 | 49 | ||
| 50 | interface NativePrompt { | 50 | interface NativePrompt { |
| 51 | readonly index: number; | 51 | readonly index: number; |
| 52 | readonly content: ReadonlyArray<AcpSchema.ContentBlock>; | ||
| 52 | readonly result: Deferred.Deferred<AcpSchema.PromptResponse, AcpErrors.AcpError>; | 53 | readonly result: Deferred.Deferred<AcpSchema.PromptResponse, AcpErrors.AcpError>; |
| 53 | } | 54 | } |
| 54 | 55 | ||
| ... | @@ -179,12 +180,13 @@ const makeHarness = Effect.fn("makeAntigravityAdapterHarness")(function* (option | ... | @@ -179,12 +180,13 @@ const makeHarness = Effect.fn("makeAntigravityAdapterHarness")(function* (option |
| 179 | }), | 180 | }), |
| 180 | getEvents: () => Stream.fromQueue(runtimeEvents), | 181 | getEvents: () => Stream.fromQueue(runtimeEvents), |
| 181 | drainEvents, | 182 | drainEvents, |
| 182 | prompt: (_payload, promptOptions) => | 183 | prompt: (payload, promptOptions) => |
| 183 | Effect.gen(function* () { | 184 | Effect.gen(function* () { |
| 184 | yield* Deferred.succeed(dispatchStarted, undefined); | 185 | yield* Deferred.succeed(dispatchStarted, undefined); |
| 185 | if (options?.holdDispatch) yield* Deferred.await(dispatchRelease); | 186 | if (options?.holdDispatch) yield* Deferred.await(dispatchRelease); |
| 186 | const prompt: NativePrompt = { | 187 | const prompt: NativePrompt = { |
| 187 | index: ++promptIndex, | 188 | index: ++promptIndex, |
| 189 | content: payload.prompt, | ||
| 188 | result: yield* Deferred.make<AcpSchema.PromptResponse, AcpErrors.AcpError>(), | 190 | result: yield* Deferred.make<AcpSchema.PromptResponse, AcpErrors.AcpError>(), |
| 189 | }; | 191 | }; |
| 190 | active = prompt; | 192 | active = prompt; |
| ... | @@ -597,7 +599,11 @@ it.layer(layer)("AntigravityAdapter", (it) => { | ... | @@ -597,7 +599,11 @@ it.layer(layer)("AntigravityAdapter", (it) => { |
| 597 | const first = yield* h.adapter | 599 | const first = yield* h.adapter |
| 598 | .sendTurn({ threadId, input: "First prompt" }) | 600 | .sendTurn({ threadId, input: "First prompt" }) |
| 599 | .pipe(Effect.forkChild); | 601 | .pipe(Effect.forkChild); |
| 600 | yield* h.nextPrompt; | 602 | const initialPrompt = yield* h.nextPrompt; |
| 603 | expect(initialPrompt.content).toEqual([ | ||
| 604 | { type: "text", text: "First prompt" }, | ||
| 605 | { type: "text", text: expect.stringContaining(`Antigravity harness, as ${nativeDefault}`) }, | ||
| 606 | ]); | ||
| 601 | const marker = h.calls.length; | 607 | const marker = h.calls.length; |
| 602 | const second = yield* h.adapter | 608 | const second = yield* h.adapter |
| 603 | .sendTurn({ | 609 | .sendTurn({ |
| ... | @@ -615,6 +621,13 @@ it.layer(layer)("AntigravityAdapter", (it) => { | ... | @@ -615,6 +621,13 @@ it.layer(layer)("AntigravityAdapter", (it) => { |
| 615 | }); | 621 | }); |
| 616 | yield* Deferred.succeed(h.cancelRelease, undefined); | 622 | yield* Deferred.succeed(h.cancelRelease, undefined); |
| 617 | const replacement = yield* h.nextPrompt; | 623 | const replacement = yield* h.nextPrompt; |
| 624 | expect(replacement.content).toEqual([ | ||
| 625 | { type: "text", text: "Steer the turn" }, | ||
| 626 | { | ||
| 627 | type: "text", | ||
| 628 | text: expect.stringContaining(`Antigravity harness, as ${nativeAlternative}`), | ||
| 629 | }, | ||
| 630 | ]); | ||
| 618 | expect(h.calls.slice(marker)).toEqual([ | 631 | expect(h.calls.slice(marker)).toEqual([ |
| 619 | "cancel:1", | 632 | "cancel:1", |
| 620 | "drained:1", | 633 | "drained:1", |
apps/server/src/provider/Layers/AntigravityAdapter.ts+13-1| ... | @@ -36,6 +36,7 @@ import * as EffectAcpErrors from "effect-acp/errors"; | ... | @@ -36,6 +36,7 @@ import * as EffectAcpErrors from "effect-acp/errors"; |
| 36 | import type * as EffectAcpSchema from "effect-acp/schema"; | 36 | import type * as EffectAcpSchema from "effect-acp/schema"; |
| 37 | 37 | ||
| 38 | import { ServerConfig } from "../../config.ts"; | 38 | import { ServerConfig } from "../../config.ts"; |
| 39 | import { buildRuntimeInstructions } from "../RuntimeInstructions.ts"; | ||
| 39 | import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; | 40 | import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; |
| 40 | import type { AntigravityAuth } from "../AntigravityAuth.ts"; | 41 | import type { AntigravityAuth } from "../AntigravityAuth.ts"; |
| 41 | import { | 42 | import { |
| ... | @@ -1075,7 +1076,18 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi | ... | @@ -1075,7 +1076,18 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi |
| 1075 | }; | 1076 | }; |
| 1076 | const dispatched = yield* Deferred.make<void>(); | 1077 | const dispatched = yield* Deferred.make<void>(); |
| 1077 | const fiber = yield* context.runtime | 1078 | const fiber = yield* context.runtime |
| 1078 | .prompt({ prompt }, { dispatched }) | 1079 | .prompt( |
| 1080 | { | ||
| 1081 | prompt: [ | ||
| 1082 | ...prompt, | ||
| 1083 | { | ||
| 1084 | type: "text", | ||
| 1085 | text: buildRuntimeInstructions({ harness: "Antigravity", model }), | ||
| 1086 | }, | ||
| 1087 | ], | ||
| 1088 | }, | ||
| 1089 | { dispatched }, | ||
| 1090 | ) | ||
| 1079 | .pipe(Effect.forkIn(context.scope)); | 1091 | .pipe(Effect.forkIn(context.scope)); |
| 1080 | context.promptFiber = fiber; | 1092 | context.promptFiber = fiber; |
| 1081 | // Fiber.join can skip a scope-close waiter when the child is interrupted. | 1093 | // Fiber.join can skip a scope-close waiter when the child is interrupted. |
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+6| ... | @@ -385,6 +385,12 @@ describe("ClaudeAdapterLive", () => { | ... | @@ -385,6 +385,12 @@ describe("ClaudeAdapterLive", () => { |
| 385 | 385 | ||
| 386 | const createInput = harness.getLastCreateQueryInput(); | 386 | const createInput = harness.getLastCreateQueryInput(); |
| 387 | assert.deepEqual(createInput?.options.settingSources, ["user", "project", "local"]); | 387 | assert.deepEqual(createInput?.options.settingSources, ["user", "project", "local"]); |
| 388 | assert.deepEqual(createInput?.options.systemPrompt, { | ||
| 389 | type: "preset", | ||
| 390 | preset: "claude_code", | ||
| 391 | append: | ||
| 392 | "<runtime_info>In case you're asked: you are running in T3 Code through the Claude Code harness. No need to mention this otherwise. You can embed images and videos in your response using Markdown with absolute file paths.</runtime_info>", | ||
| 393 | }); | ||
| 388 | assert.equal(createInput?.options.permissionMode, "bypassPermissions"); | 394 | assert.equal(createInput?.options.permissionMode, "bypassPermissions"); |
| 389 | assert.equal(createInput?.options.allowDangerouslySkipPermissions, true); | 395 | assert.equal(createInput?.options.allowDangerouslySkipPermissions, true); |
| 390 | }).pipe( | 396 | }).pipe( |
apps/server/src/provider/Layers/ClaudeAdapter.ts+7-1| ... | @@ -83,6 +83,7 @@ import { resolveClaudeSdkExecutablePath } from "../Drivers/ClaudeExecutable.ts"; | ... | @@ -83,6 +83,7 @@ import { resolveClaudeSdkExecutablePath } from "../Drivers/ClaudeExecutable.ts"; |
| 83 | import { makeClaudeEnvironment } from "../Drivers/ClaudeHome.ts"; | 83 | import { makeClaudeEnvironment } from "../Drivers/ClaudeHome.ts"; |
| 84 | import { planClaudeSkillDispatch } from "../Drivers/ClaudeSkillDispatch.ts"; | 84 | import { planClaudeSkillDispatch } from "../Drivers/ClaudeSkillDispatch.ts"; |
| 85 | import { discoverClaudeSkills } from "../Drivers/ClaudeSkills.ts"; | 85 | import { discoverClaudeSkills } from "../Drivers/ClaudeSkills.ts"; |
| 86 | import { buildRuntimeInstructions } from "../RuntimeInstructions.ts"; | ||
| 86 | import { | 87 | import { |
| 87 | BUNDLED_CLAUDE_MODEL_CATALOG, | 88 | BUNDLED_CLAUDE_MODEL_CATALOG, |
| 88 | type ClaudeModelCatalog, | 89 | type ClaudeModelCatalog, |
| ... | @@ -4370,7 +4371,12 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( | ... | @@ -4370,7 +4371,12 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 4370 | ...(input.cwd ? { cwd: input.cwd } : {}), | 4371 | ...(input.cwd ? { cwd: input.cwd } : {}), |
| 4371 | ...(apiModelId ? { model: apiModelId } : {}), | 4372 | ...(apiModelId ? { model: apiModelId } : {}), |
| 4372 | pathToClaudeCodeExecutable: claudeBinaryPath, | 4373 | pathToClaudeCodeExecutable: claudeBinaryPath, |
| 4373 | systemPrompt: { type: "preset", preset: "claude_code" }, | 4374 | systemPrompt: { |
| 4375 | type: "preset", | ||
| 4376 | preset: "claude_code", | ||
| 4377 | // Model and effort can change after this session-level prompt is set. | ||
| 4378 | append: buildRuntimeInstructions({ harness: "Claude Code" }), | ||
| 4379 | }, | ||
| 4374 | settingSources: [...CLAUDE_SETTING_SOURCES], | 4380 | settingSources: [...CLAUDE_SETTING_SOURCES], |
| 4375 | // `ultracode` is a Claude Code setting, not an API effort level. It is | 4381 | // `ultracode` is a Claude Code setting, not an API effort level. It is |
| 4376 | // normalized to `xhigh` above and paired with `settings.ultracode`. | 4382 | // normalized to `xhigh` above and paired with `settings.ultracode`. |
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts+13| ... | @@ -465,6 +465,19 @@ describe("buildCodexDeveloperInstructions", () => { | ... | @@ -465,6 +465,19 @@ describe("buildCodexDeveloperInstructions", () => { |
| 465 | NodeAssert.match(instructions, /as gpt-5\.3-codex with high reasoning effort/); | 465 | NodeAssert.match(instructions, /as gpt-5\.3-codex with high reasoning effort/); |
| 466 | }); | 466 | }); |
| 467 | 467 | ||
| 468 | it("describes Markdown media support in the runtime context in both modes", () => { | ||
| 469 | for (const mode of ["default", "plan"] as const) { | ||
| 470 | const instructions = buildCodexDeveloperInstructions(mode, { | ||
| 471 | model: "gpt-5.3-codex", | ||
| 472 | reasoningEffort: "high", | ||
| 473 | }); | ||
| 474 | NodeAssert.match( | ||
| 475 | instructions, | ||
| 476 | /<runtime_info>.*embed images and videos.*Markdown.*<\/runtime_info>/, | ||
| 477 | ); | ||
| 478 | } | ||
| 479 | }); | ||
| 480 | |||
| 468 | it("includes runtime info alongside plan mode instructions", () => { | 481 | it("includes runtime info alongside plan mode instructions", () => { |
| 469 | const instructions = buildCodexDeveloperInstructions("plan", { | 482 | const instructions = buildCodexDeveloperInstructions("plan", { |
| 470 | model: "gpt-5.3-codex", | 483 | model: "gpt-5.3-codex", |
apps/server/src/provider/Layers/CursorAdapter.test.ts+19-1| ... | @@ -26,6 +26,7 @@ import { | ... | @@ -26,6 +26,7 @@ import { |
| 26 | } from "@t3tools/contracts"; | 26 | } from "@t3tools/contracts"; |
| 27 | 27 | ||
| 28 | import { ServerConfig } from "../../config.ts"; | 28 | import { ServerConfig } from "../../config.ts"; |
| 29 | import { buildRuntimeInstructions } from "../RuntimeInstructions.ts"; | ||
| 29 | import { ServerSettingsService } from "../../serverSettings.ts"; | 30 | import { ServerSettingsService } from "../../serverSettings.ts"; |
| 30 | import type { CursorAdapterShape } from "../Services/CursorAdapter.ts"; | 31 | import type { CursorAdapterShape } from "../Services/CursorAdapter.ts"; |
| 31 | import { makeCursorAdapter } from "./CursorAdapter.ts"; | 32 | import { makeCursorAdapter } from "./CursorAdapter.ts"; |
| ... | @@ -283,6 +284,18 @@ cursorAdapterTestLayer("CursorAdapterLive", (it) => { | ... | @@ -283,6 +284,18 @@ cursorAdapterTestLayer("CursorAdapterLive", (it) => { |
| 283 | input: "please $review this", | 284 | input: "please $review this", |
| 284 | attachments: [], | 285 | attachments: [], |
| 285 | }); | 286 | }); |
| 287 | const snapshot = yield* adapter.readThread(threadId); | ||
| 288 | assert.deepStrictEqual( | ||
| 289 | snapshot.turns.map((turn) => turn.items), | ||
| 290 | [ | ||
| 291 | [ | ||
| 292 | { | ||
| 293 | prompt: [{ type: "text", text: "please /review this" }], | ||
| 294 | result: { stopReason: "end_turn" }, | ||
| 295 | }, | ||
| 296 | ], | ||
| 297 | ], | ||
| 298 | ); | ||
| 286 | yield* adapter.stopSession(threadId); | 299 | yield* adapter.stopSession(threadId); |
| 287 | 300 | ||
| 288 | const requests = yield* Effect.promise(() => readJsonLines(requestLogPath)); | 301 | const requests = yield* Effect.promise(() => readJsonLines(requestLogPath)); |
| ... | @@ -291,7 +304,12 @@ cursorAdapterTestLayer("CursorAdapterLive", (it) => { | ... | @@ -291,7 +304,12 @@ cursorAdapterTestLayer("CursorAdapterLive", (it) => { |
| 291 | promptRequests.map( | 304 | promptRequests.map( |
| 292 | (request) => (request.params as Record<string, unknown> | undefined)?.prompt, | 305 | (request) => (request.params as Record<string, unknown> | undefined)?.prompt, |
| 293 | ), | 306 | ), |
| 294 | [[{ type: "text", text: "please /review this" }]], | 307 | [ |
| 308 | [ | ||
| 309 | { type: "text", text: "please /review this" }, | ||
| 310 | { type: "text", text: buildRuntimeInstructions({ harness: "Cursor" }) }, | ||
| 311 | ], | ||
| 312 | ], | ||
| 295 | ); | 313 | ); |
| 296 | }), | 314 | }), |
| 297 | ); | 315 | ); |
apps/server/src/provider/Layers/CursorAdapter.ts+9-1| ... | @@ -42,6 +42,7 @@ import type * as EffectAcpSchema from "effect-acp/schema"; | ... | @@ -42,6 +42,7 @@ import type * as EffectAcpSchema from "effect-acp/schema"; |
| 42 | 42 | ||
| 43 | import { resolveAttachmentPath } from "../../attachmentStore.ts"; | 43 | import { resolveAttachmentPath } from "../../attachmentStore.ts"; |
| 44 | import { ServerConfig } from "../../config.ts"; | 44 | import { ServerConfig } from "../../config.ts"; |
| 45 | import { buildRuntimeInstructions } from "../RuntimeInstructions.ts"; | ||
| 45 | import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; | 46 | import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; |
| 46 | import { | 47 | import { |
| 47 | ProviderAdapterProcessError, | 48 | ProviderAdapterProcessError, |
| ... | @@ -1043,9 +1044,16 @@ export function makeCursorAdapter( | ... | @@ -1043,9 +1044,16 @@ export function makeCursorAdapter( |
| 1043 | }); | 1044 | }); |
| 1044 | } | 1045 | } |
| 1045 | 1046 | ||
| 1047 | // ACP has no system-message field; keep runtime context separate from the user's text. | ||
| 1046 | const result = yield* ctx.acp | 1048 | const result = yield* ctx.acp |
| 1047 | .prompt({ | 1049 | .prompt({ |
| 1048 | prompt: promptParts, | 1050 | prompt: [ |
| 1051 | ...promptParts, | ||
| 1052 | { | ||
| 1053 | type: "text", | ||
| 1054 | text: buildRuntimeInstructions({ harness: "Cursor", model: resolvedModel }), | ||
| 1055 | }, | ||
| 1056 | ], | ||
| 1049 | }) | 1057 | }) |
| 1050 | .pipe( | 1058 | .pipe( |
| 1051 | Effect.mapError((error) => | 1059 | Effect.mapError((error) => |
apps/server/src/provider/Layers/GrokAdapter.test.ts+62| ... | @@ -213,6 +213,68 @@ it("requires a settlement to match the live Grok turn", () => { | ... | @@ -213,6 +213,68 @@ it("requires a settlement to match the live Grok turn", () => { |
| 213 | }); | 213 | }); |
| 214 | 214 | ||
| 215 | it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => { | 215 | it.layer(grokAdapterTestLayer)("GrokAdapterLive", (it) => { |
| 216 | it.effect("sends runtime context with the current model without changing saved prompts", () => | ||
| 217 | Effect.gen(function* () { | ||
| 218 | const threadId = ThreadId.make("grok-runtime-context"); | ||
| 219 | const tempDir = yield* Effect.promise(() => | ||
| 220 | NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "grok-runtime-context-")), | ||
| 221 | ); | ||
| 222 | const requestLogPath = NodePath.join(tempDir, "requests.ndjson"); | ||
| 223 | const wrapperPath = yield* Effect.promise(() => | ||
| 224 | makeMockGrokWrapper({ T3_ACP_REQUEST_LOG_PATH: requestLogPath }), | ||
| 225 | ); | ||
| 226 | const adapter = yield* makeTestAdapter(wrapperPath); | ||
| 227 | yield* adapter.startSession({ | ||
| 228 | threadId, | ||
| 229 | cwd: process.cwd(), | ||
| 230 | runtimeMode: "full-access", | ||
| 231 | modelSelection: { instanceId: ProviderInstanceId.make("grok"), model: "grok-mock-alt" }, | ||
| 232 | }); | ||
| 233 | yield* adapter.sendTurn({ threadId, input: "First prompt" }); | ||
| 234 | yield* adapter.sendTurn({ | ||
| 235 | threadId, | ||
| 236 | input: "Second prompt", | ||
| 237 | modelSelection: { | ||
| 238 | instanceId: ProviderInstanceId.make("grok"), | ||
| 239 | model: "grok-4.6", | ||
| 240 | options: [{ id: "reasoningEffort", value: "low" }], | ||
| 241 | }, | ||
| 242 | }); | ||
| 243 | const snapshot = yield* adapter.readThread(threadId); | ||
| 244 | assert.deepEqual( | ||
| 245 | snapshot.turns.map((turn) => turn.items), | ||
| 246 | [ | ||
| 247 | [ | ||
| 248 | { | ||
| 249 | prompt: [{ type: "text", text: "First prompt" }], | ||
| 250 | result: { stopReason: "end_turn" }, | ||
| 251 | }, | ||
| 252 | ], | ||
| 253 | [ | ||
| 254 | { | ||
| 255 | prompt: [{ type: "text", text: "Second prompt" }], | ||
| 256 | result: { stopReason: "end_turn" }, | ||
| 257 | }, | ||
| 258 | ], | ||
| 259 | ], | ||
| 260 | ); | ||
| 261 | yield* adapter.stopSession(threadId); | ||
| 262 | const requests = yield* Effect.promise(() => readJsonLines(requestLogPath)); | ||
| 263 | const prompts = requests | ||
| 264 | .filter((request) => request.method === "session/prompt") | ||
| 265 | .map( | ||
| 266 | (request) => (request.params as { prompt: Array<{ type: string; text: string }> }).prompt, | ||
| 267 | ); | ||
| 268 | assert.equal(prompts.length, 2); | ||
| 269 | assert.deepEqual(prompts[0]?.[0], { type: "text", text: "First prompt" }); | ||
| 270 | assert.include(prompts[0]?.[1]?.text, "Grok harness, as grok-mock-alt"); | ||
| 271 | assert.deepEqual(prompts[1]?.[0], { type: "text", text: "Second prompt" }); | ||
| 272 | assert.include(prompts[1]?.[1]?.text, "Grok harness, as grok-4.6"); | ||
| 273 | assert.include(prompts[1]?.[1]?.text, "with low reasoning effort"); | ||
| 274 | assert.include(prompts[1]?.[1]?.text, "embed images and videos"); | ||
| 275 | }), | ||
| 276 | ); | ||
| 277 | |||
| 216 | it.effect("starts a session and maps mock ACP prompt flow to runtime events", () => | 278 | it.effect("starts a session and maps mock ACP prompt flow to runtime events", () => |
| 217 | Effect.gen(function* () { | 279 | Effect.gen(function* () { |
| 218 | const threadId = ThreadId.make("grok-mock-thread"); | 280 | const threadId = ThreadId.make("grok-mock-thread"); |
apps/server/src/provider/Layers/GrokAdapter.ts+16-1| ... | @@ -40,6 +40,7 @@ import type * as EffectAcpSchema from "effect-acp/schema"; | ... | @@ -40,6 +40,7 @@ import type * as EffectAcpSchema from "effect-acp/schema"; |
| 40 | 40 | ||
| 41 | import { resolveAttachmentPath } from "../../attachmentStore.ts"; | 41 | import { resolveAttachmentPath } from "../../attachmentStore.ts"; |
| 42 | import { ServerConfig } from "../../config.ts"; | 42 | import { ServerConfig } from "../../config.ts"; |
| 43 | import { buildRuntimeInstructions } from "../RuntimeInstructions.ts"; | ||
| 43 | import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; | 44 | import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; |
| 44 | import { | 45 | import { |
| 45 | ProviderAdapterProcessError, | 46 | ProviderAdapterProcessError, |
| ... | @@ -1583,6 +1584,11 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte | ... | @@ -1583,6 +1584,11 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte |
| 1583 | const displayModel = currentModelId | 1584 | const displayModel = currentModelId |
| 1584 | ? resolveGrokAcpBaseModelId(currentModelId) | 1585 | ? resolveGrokAcpBaseModelId(currentModelId) |
| 1585 | : undefined; | 1586 | : undefined; |
| 1587 | const runtimeInstructions = buildRuntimeInstructions({ | ||
| 1588 | harness: "Grok", | ||
| 1589 | model: displayModel, | ||
| 1590 | reasoningEffort: normalizeGrokReasoningEffort(requestedTurnReasoningEffort), | ||
| 1591 | }); | ||
| 1586 | for (let yieldAttempt = 0; yieldAttempt < 8; yieldAttempt += 1) { | 1592 | for (let yieldAttempt = 0; yieldAttempt < 8; yieldAttempt += 1) { |
| 1587 | yield* Effect.yieldNow; | 1593 | yield* Effect.yieldNow; |
| 1588 | } | 1594 | } |
| ... | @@ -1638,6 +1644,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte | ... | @@ -1638,6 +1644,7 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte |
| 1638 | acpSessionId: ctx.acpSessionId, | 1644 | acpSessionId: ctx.acpSessionId, |
| 1639 | displayModel, | 1645 | displayModel, |
| 1640 | promptParts, | 1646 | promptParts, |
| 1647 | runtimeInstructions, | ||
| 1641 | turnId, | 1648 | turnId, |
| 1642 | promptEpoch, | 1649 | promptEpoch, |
| 1643 | promptLifecycle: ctx.promptLifecycle, | 1650 | promptLifecycle: ctx.promptLifecycle, |
| ... | @@ -1694,7 +1701,15 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte | ... | @@ -1694,7 +1701,15 @@ export function makeGrokAdapter(grokSettings: GrokSettings, options?: GrokAdapte |
| 1694 | } | 1701 | } |
| 1695 | const dispatched = yield* Deferred.make<void>(); | 1702 | const dispatched = yield* Deferred.make<void>(); |
| 1696 | const fiber = yield* liveCtx.acp | 1703 | const fiber = yield* liveCtx.acp |
| 1697 | .prompt({ prompt: prepared.promptParts }, { dispatched }) | 1704 | .prompt( |
| 1705 | { | ||
| 1706 | prompt: [ | ||
| 1707 | ...prepared.promptParts, | ||
| 1708 | { type: "text", text: prepared.runtimeInstructions }, | ||
| 1709 | ], | ||
| 1710 | }, | ||
| 1711 | { dispatched }, | ||
| 1712 | ) | ||
| 1698 | .pipe(Effect.forkChild({ startImmediately: true })); | 1713 | .pipe(Effect.forkChild({ startImmediately: true })); |
| 1699 | // Hold the lifecycle permit until the runtime has registered this | 1714 | // Hold the lifecycle permit until the runtime has registered this |
| 1700 | // prompt's RPC fiber, so a later steer's session/cancel targets | 1715 | // prompt's RPC fiber, so a later steer's session/cancel targets |
apps/server/src/provider/Layers/OpenCodeAdapter.test.ts+9| ... | @@ -27,6 +27,7 @@ import { | ... | @@ -27,6 +27,7 @@ import { |
| 27 | import { createModelSelection } from "@t3tools/shared/model"; | 27 | import { createModelSelection } from "@t3tools/shared/model"; |
| 28 | import { ServerConfig } from "../../config.ts"; | 28 | import { ServerConfig } from "../../config.ts"; |
| 29 | import { ServerSettingsService } from "../../serverSettings.ts"; | 29 | import { ServerSettingsService } from "../../serverSettings.ts"; |
| 30 | import { buildRuntimeInstructions } from "../RuntimeInstructions.ts"; | ||
| 30 | import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; | 31 | import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; |
| 31 | import type { OpenCodeAdapterShape } from "../Services/OpenCodeAdapter.ts"; | 32 | import type { OpenCodeAdapterShape } from "../Services/OpenCodeAdapter.ts"; |
| 32 | import { | 33 | import { |
| ... | @@ -4958,6 +4959,10 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { | ... | @@ -4958,6 +4959,10 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { |
| 4958 | }, | 4959 | }, |
| 4959 | agent: "github-copilot", | 4960 | agent: "github-copilot", |
| 4960 | variant: "high", | 4961 | variant: "high", |
| 4962 | system: buildRuntimeInstructions({ | ||
| 4963 | harness: "OpenCode", | ||
| 4964 | model: "anthropic/claude-sonnet-4-5", | ||
| 4965 | }), | ||
| 4961 | parts: [{ type: "text", text: "Fix it" }], | 4966 | parts: [{ type: "text", text: "Fix it" }], |
| 4962 | }); | 4967 | }); |
| 4963 | }).pipe(Effect.provide(adapterLayer)); | 4968 | }).pipe(Effect.provide(adapterLayer)); |
| ... | @@ -5005,6 +5010,10 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { | ... | @@ -5005,6 +5010,10 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => { |
| 5005 | providerID: "anthropic", | 5010 | providerID: "anthropic", |
| 5006 | modelID: "claude-sonnet-4-5", | 5011 | modelID: "claude-sonnet-4-5", |
| 5007 | }, | 5012 | }, |
| 5013 | system: buildRuntimeInstructions({ | ||
| 5014 | harness: "OpenCode", | ||
| 5015 | model: "anthropic/claude-sonnet-4-5", | ||
| 5016 | }), | ||
| 5008 | parts: [{ type: "text", text: "Fix it" }], | 5017 | parts: [{ type: "text", text: "Fix it" }], |
| 5009 | }); | 5018 | }); |
| 5010 | }).pipe(Effect.provide(adapterLayer)); | 5019 | }).pipe(Effect.provide(adapterLayer)); |
apps/server/src/provider/Layers/OpenCodeAdapter.ts+6| ... | @@ -41,6 +41,7 @@ import { | ... | @@ -41,6 +41,7 @@ import { |
| 41 | ProviderAdapterSessionNotFoundError, | 41 | ProviderAdapterSessionNotFoundError, |
| 42 | ProviderAdapterValidationError, | 42 | ProviderAdapterValidationError, |
| 43 | } from "../Errors.ts"; | 43 | } from "../Errors.ts"; |
| 44 | import { buildRuntimeInstructions } from "../RuntimeInstructions.ts"; | ||
| 44 | import { type OpenCodeAdapterShape } from "../Services/OpenCodeAdapter.ts"; | 45 | import { type OpenCodeAdapterShape } from "../Services/OpenCodeAdapter.ts"; |
| 45 | import { | 46 | import { |
| 46 | buildOpenCodePermissionRules, | 47 | buildOpenCodePermissionRules, |
| ... | @@ -2856,6 +2857,11 @@ export function makeOpenCodeAdapter( | ... | @@ -2856,6 +2857,11 @@ export function makeOpenCodeAdapter( |
| 2856 | model: parsedModel, | 2857 | model: parsedModel, |
| 2857 | ...(context.activeAgent ? { agent: context.activeAgent } : {}), | 2858 | ...(context.activeAgent ? { agent: context.activeAgent } : {}), |
| 2858 | ...(context.activeVariant ? { variant: context.activeVariant } : {}), | 2859 | ...(context.activeVariant ? { variant: context.activeVariant } : {}), |
| 2860 | // OpenCode appends this after its own agent/provider prompts. | ||
| 2861 | system: buildRuntimeInstructions({ | ||
| 2862 | harness: "OpenCode", | ||
| 2863 | model: `${parsedModel.providerID}/${parsedModel.modelID}`, | ||
| 2864 | }), | ||
| 2859 | parts: [...(text ? [{ type: "text" as const, text }] : []), ...fileParts], | 2865 | parts: [...(text ? [{ type: "text" as const, text }] : []), ...fileParts], |
| 2860 | }, | 2866 | }, |
| 2861 | { signal }, | 2867 | { signal }, |
apps/server/src/provider/RuntimeInstructions.test.ts created+31| ... | @@ -0,0 +1,31 @@ | ||
| 1 | import { describe, expect, it } from "vite-plus/test"; | ||
| 2 | import { buildRuntimeInstructions } from "./RuntimeInstructions.ts"; | ||
| 3 | |||
| 4 | describe("buildRuntimeInstructions", () => { | ||
| 5 | it.each(["Codex", "Claude Code", "Cursor", "Grok", "OpenCode", "Antigravity"])( | ||
| 6 | "identifies the %s harness and describes media embedding", | ||
| 7 | (harness) => { | ||
| 8 | const instructions = buildRuntimeInstructions({ harness }); | ||
| 9 | expect(instructions).toContain(`running in T3 Code through the ${harness} harness.`); | ||
| 10 | expect(instructions).toContain("embed images and videos"); | ||
| 11 | expect(instructions).toContain("Markdown with absolute file paths"); | ||
| 12 | expect(instructions).not.toContain("undefined"); | ||
| 13 | }, | ||
| 14 | ); | ||
| 15 | |||
| 16 | it("keeps known model and effort metadata on one line", () => { | ||
| 17 | expect( | ||
| 18 | buildRuntimeInstructions({ | ||
| 19 | harness: "Codex", | ||
| 20 | model: " custom\nmodel ", | ||
| 21 | reasoningEffort: " high\n", | ||
| 22 | }), | ||
| 23 | ).toContain("through the Codex harness, as custom model with high reasoning effort."); | ||
| 24 | }); | ||
| 25 | |||
| 26 | it.each([undefined, "", "auto", "default"])("omits unresolved model %s", (model) => { | ||
| 27 | const instructions = buildRuntimeInstructions({ harness: "Cursor", model }); | ||
| 28 | expect(instructions).toContain("through the Cursor harness."); | ||
| 29 | expect(instructions).not.toContain("reasoning effort"); | ||
| 30 | }); | ||
| 31 | }); | ||
apps/server/src/provider/RuntimeInstructions.ts created+17| ... | @@ -0,0 +1,17 @@ | ||
| 1 | /** Shared runtime context; omit model and effort when the harness manages them dynamically. */ | ||
| 2 | export function buildRuntimeInstructions(runtime: { | ||
| 3 | readonly harness: string; | ||
| 4 | readonly model?: string | undefined; | ||
| 5 | readonly reasoningEffort?: string | undefined; | ||
| 6 | }): string { | ||
| 7 | const harness = toSingleLine(runtime.harness); | ||
| 8 | const model = toSingleLine(runtime.model ?? ""); | ||
| 9 | const effort = toSingleLine(runtime.reasoningEffort ?? ""); | ||
| 10 | const modelInfo = model && model !== "auto" && model !== "default" ? `, as ${model}` : ""; | ||
| 11 | const effortInfo = effort ? ` with ${effort} reasoning effort` : ""; | ||
| 12 | 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>`; | ||
| 13 | } | ||
| 14 | |||
| 15 | function toSingleLine(value: string): string { | ||
| 16 | return value.replaceAll(/\s+/g, " ").trim(); | ||
| 17 | } | ||
apps/web/src/components/chat/MessagesTimeline.logic.test.ts-136| ... | @@ -12,7 +12,6 @@ import { | ... | @@ -12,7 +12,6 @@ import { |
| 12 | shouldPreserveAssistantLineBreaks, | 12 | shouldPreserveAssistantLineBreaks, |
| 13 | type MessagesTimelineRow, | 13 | type MessagesTimelineRow, |
| 14 | workEntryDisplayLabel, | 14 | workEntryDisplayLabel, |
| 15 | workEntryIsVisibleInGroup, | ||
| 16 | } from "./MessagesTimeline.logic"; | 15 | } from "./MessagesTimeline.logic"; |
| 17 | 16 | ||
| 18 | describe("expanded tool group scrolling", () => { | 17 | describe("expanded tool group scrolling", () => { |
| ... | @@ -472,22 +471,6 @@ describe("resolveAssistantMessageCopyState", () => { | ... | @@ -472,22 +471,6 @@ describe("resolveAssistantMessageCopyState", () => { |
| 472 | }); | 471 | }); |
| 473 | }); | 472 | }); |
| 474 | 473 | ||
| 475 | describe("workEntryIsVisibleInGroup", () => { | ||
| 476 | it("keeps an in-progress image preview row visible outside a group", () => { | ||
| 477 | const entry = { | ||
| 478 | id: "work-image", | ||
| 479 | createdAt: "2026-01-01T00:00:04Z", | ||
| 480 | turnId: "turn-1" as never, | ||
| 481 | label: "Image view", | ||
| 482 | detail: "screenshots/result.png", | ||
| 483 | itemType: "image_view" as const, | ||
| 484 | tone: "tool" as const, | ||
| 485 | toolLifecycleStatus: "inProgress" as const, | ||
| 486 | }; | ||
| 487 | expect(workEntryIsVisibleInGroup(entry, false)).toBe(true); | ||
| 488 | }); | ||
| 489 | }); | ||
| 490 | |||
| 491 | describe("deriveMessagesTimelineRows", () => { | 474 | describe("deriveMessagesTimelineRows", () => { |
| 492 | it("keeps context compaction visible outside folded work", () => { | 475 | it("keeps context compaction visible outside folded work", () => { |
| 493 | const rows = deriveMessagesTimelineRows({ | 476 | const rows = deriveMessagesTimelineRows({ |
| ... | @@ -883,125 +866,6 @@ describe("deriveMessagesTimelineRows", () => { | ... | @@ -883,125 +866,6 @@ describe("deriveMessagesTimelineRows", () => { |
| 883 | }); | 866 | }); |
| 884 | }); | 867 | }); |
| 885 | 868 | ||
| 886 | it("keeps an image preview row visible after the turn settles", () => { | ||
| 887 | const timelineEntries = [ | ||
| 888 | { | ||
| 889 | id: "work-command-entry", | ||
| 890 | kind: "work" as const, | ||
| 891 | createdAt: "2026-01-01T00:00:02Z", | ||
| 892 | entry: { | ||
| 893 | id: "work-command", | ||
| 894 | createdAt: "2026-01-01T00:00:02Z", | ||
| 895 | turnId: "turn-1" as never, | ||
| 896 | label: "Ran command", | ||
| 897 | tone: "tool" as const, | ||
| 898 | }, | ||
| 899 | }, | ||
| 900 | { | ||
| 901 | id: "work-image-entry", | ||
| 902 | kind: "work" as const, | ||
| 903 | createdAt: "2026-01-01T00:00:04Z", | ||
| 904 | entry: { | ||
| 905 | id: "work-image", | ||
| 906 | createdAt: "2026-01-01T00:00:04Z", | ||
| 907 | turnId: "turn-1" as never, | ||
| 908 | label: "Image view", | ||
| 909 | detail: "screenshots/result.png", | ||
| 910 | itemType: "image_view" as const, | ||
| 911 | tone: "tool" as const, | ||
| 912 | }, | ||
| 913 | }, | ||
| 914 | { | ||
| 915 | id: "assistant-final-entry", | ||
| 916 | kind: "message" as const, | ||
| 917 | createdAt: "2026-01-01T00:00:06Z", | ||
| 918 | message: { | ||
| 919 | id: "assistant-final" as never, | ||
| 920 | role: "assistant" as const, | ||
| 921 | text: "Here is the screenshot.", | ||
| 922 | turnId: "turn-1" as never, | ||
| 923 | createdAt: "2026-01-01T00:00:06Z", | ||
| 924 | updatedAt: "2026-01-01T00:00:07Z", | ||
| 925 | streaming: false, | ||
| 926 | }, | ||
| 927 | }, | ||
| 928 | ]; | ||
| 929 | |||
| 930 | const rows = deriveMessagesTimelineRows({ | ||
| 931 | timelineEntries, | ||
| 932 | isWorking: false, | ||
| 933 | activeTurnStartedAt: null, | ||
| 934 | turnDiffSummaryByAssistantMessageId: new Map(), | ||
| 935 | revertTurnCountByUserMessageId: new Map(), | ||
| 936 | }); | ||
| 937 | |||
| 938 | // The command row folds, the image row stays as its own visible row rather | ||
| 939 | // than collapsing into a tool group summary. | ||
| 940 | expect(rows.map((row) => row.id)).toEqual([ | ||
| 941 | "turn-fold:turn-1", | ||
| 942 | "work-image-entry", | ||
| 943 | "assistant-final-entry", | ||
| 944 | ]); | ||
| 945 | expect(rows.find((row) => row.id === "work-image-entry")?.kind).toBe("work"); | ||
| 946 | }); | ||
| 947 | |||
| 948 | it("keeps a trailing image preview row visible while the turn is still working", () => { | ||
| 949 | const rows = deriveMessagesTimelineRows({ | ||
| 950 | timelineEntries: [ | ||
| 951 | { | ||
| 952 | id: "user-entry", | ||
| 953 | kind: "message" as const, | ||
| 954 | createdAt: "2026-01-01T00:00:00Z", | ||
| 955 | message: { | ||
| 956 | id: "user-1" as never, | ||
| 957 | role: "user" as const, | ||
| 958 | text: "show me the result", | ||
| 959 | turnId: null, | ||
| 960 | createdAt: "2026-01-01T00:00:00Z", | ||
| 961 | updatedAt: "2026-01-01T00:00:00Z", | ||
| 962 | streaming: false, | ||
| 963 | }, | ||
| 964 | }, | ||
| 965 | { | ||
| 966 | id: "work-command-entry", | ||
| 967 | kind: "work" as const, | ||
| 968 | createdAt: "2026-01-01T00:00:02Z", | ||
| 969 | entry: { | ||
| 970 | id: "work-command", | ||
| 971 | createdAt: "2026-01-01T00:00:02Z", | ||
| 972 | turnId: "turn-1" as never, | ||
| 973 | label: "Ran command", | ||
| 974 | tone: "tool" as const, | ||
| 975 | }, | ||
| 976 | }, | ||
| 977 | { | ||
| 978 | id: "work-image-entry", | ||
| 979 | kind: "work" as const, | ||
| 980 | createdAt: "2026-01-01T00:00:04Z", | ||
| 981 | entry: { | ||
| 982 | id: "work-image", | ||
| 983 | createdAt: "2026-01-01T00:00:04Z", | ||
| 984 | turnId: "turn-1" as never, | ||
| 985 | label: "Image view", | ||
| 986 | detail: "screenshots/result.png", | ||
| 987 | itemType: "image_view" as const, | ||
| 988 | tone: "tool" as const, | ||
| 989 | }, | ||
| 990 | }, | ||
| 991 | ], | ||
| 992 | isWorking: true, | ||
| 993 | activeTurnStartedAt: "2026-01-01T00:00:01Z", | ||
| 994 | turnDiffSummaryByAssistantMessageId: new Map(), | ||
| 995 | revertTurnCountByUserMessageId: new Map(), | ||
| 996 | }); | ||
| 997 | |||
| 998 | // The live activity row must not swallow the image: it renders only a | ||
| 999 | // label, which would hide the image until the turn settles. | ||
| 1000 | const imageRow = rows.find((row) => row.id === "work-image-entry"); | ||
| 1001 | expect(imageRow?.kind).toBe("work"); | ||
| 1002 | expect(rows.some((row) => row.kind === "work-live")).toBe(false); | ||
| 1003 | }); | ||
| 1004 | |||
| 1005 | it("folds all assistant messages before the terminal message", () => { | 869 | it("folds all assistant messages before the terminal message", () => { |
| 1006 | const timelineEntries = [ | 870 | const timelineEntries = [ |
| 1007 | { | 871 | { |
apps/web/src/components/chat/MessagesTimeline.logic.ts+2-24| ... | @@ -9,7 +9,6 @@ import { | ... | @@ -9,7 +9,6 @@ import { |
| 9 | summarizeToolGroup, | 9 | summarizeToolGroup, |
| 10 | toolGroupAction, | 10 | toolGroupAction, |
| 11 | toolGroupSummaryKind, | 11 | toolGroupSummaryKind, |
| 12 | workEntryViewedImagePath, | ||
| 13 | type ToolGroupSummaryKind, | 12 | type ToolGroupSummaryKind, |
| 14 | } from "@t3tools/client-runtime/work-log/presentation"; | 13 | } from "@t3tools/client-runtime/work-log/presentation"; |
| 15 | export { | 14 | export { |
| ... | @@ -90,15 +89,6 @@ export function liveWorkEntryLabel( | ... | @@ -90,15 +89,6 @@ export function liveWorkEntryLabel( |
| 90 | return workEntryDisplayLabel(entry, workspaceRoot); | 89 | return workEntryDisplayLabel(entry, workspaceRoot); |
| 91 | } | 90 | } |
| 92 | 91 | ||
| 93 | /** | ||
| 94 | * Rows that preview an image the agent viewed or produced. They stay out of | ||
| 95 | * tool groups and out of the settled-turn fold: the image is the answer the | ||
| 96 | * user asked for, not tool noise to hide behind "Worked for ...". | ||
| 97 | */ | ||
| 98 | function workEntryRendersImagePreview(entry: WorkLogEntry): boolean { | ||
| 99 | return workEntryViewedImagePath(entry) !== null; | ||
| 100 | } | ||
| 101 | |||
| 102 | export function workEntryIsVisibleInGroup( | 92 | export function workEntryIsVisibleInGroup( |
| 103 | entry: WorkLogEntry, | 93 | entry: WorkLogEntry, |
| 104 | expandedToolGroupEntry = false, | 94 | expandedToolGroupEntry = false, |
| ... | @@ -107,9 +97,6 @@ export function workEntryIsVisibleInGroup( | ... | @@ -107,9 +97,6 @@ export function workEntryIsVisibleInGroup( |
| 107 | (expandedToolGroupEntry && | 97 | (expandedToolGroupEntry && |
| 108 | (entry.toolLifecycleStatus === "inProgress" || | 98 | (entry.toolLifecycleStatus === "inProgress" || |
| 109 | entry.sourceActivityKind === "task.progress")) || | 99 | entry.sourceActivityKind === "task.progress")) || |
| 110 | // An image row stands alone outside any group, so the neutral filter | ||
| 111 | // would leave an empty gap while its tool is still in progress. | ||
| 112 | workEntryRendersImagePreview(entry) || | ||
| 113 | !workEntryIndicatesToolNeutralStatus(entry) | 100 | !workEntryIndicatesToolNeutralStatus(entry) |
| 114 | ); | 101 | ); |
| 115 | } | 102 | } |
| ... | @@ -645,9 +632,6 @@ function deriveTurnFolds(input: { | ... | @@ -645,9 +632,6 @@ function deriveTurnFolds(input: { |
| 645 | ) { | 632 | ) { |
| 646 | continue; | 633 | continue; |
| 647 | } | 634 | } |
| 648 | if (entry.kind === "work" && workEntryRendersImagePreview(entry.entry)) { | ||
| 649 | continue; | ||
| 650 | } | ||
| 651 | hiddenEntryIds.add(entry.id); | 635 | hiddenEntryIds.add(entry.id); |
| 652 | } | 636 | } |
| 653 | if (hiddenEntryIds.size === 0) { | 637 | if (hiddenEntryIds.size === 0) { |
| ... | @@ -842,8 +826,7 @@ export function deriveMessagesTimelineRows(input: { | ... | @@ -842,8 +826,7 @@ export function deriveMessagesTimelineRows(input: { |
| 842 | entry.kind !== "work" || | 826 | entry.kind !== "work" || |
| 843 | entry.entry.agentSpawn !== undefined || | 827 | entry.entry.agentSpawn !== undefined || |
| 844 | entry.entry.sourceActivityKind === "context-compaction" || | 828 | entry.entry.sourceActivityKind === "context-compaction" || |
| 845 | entry.entry.tone === "error" || | 829 | entry.entry.tone === "error" |
| 846 | workEntryRendersImagePreview(entry.entry) | ||
| 847 | ) { | 830 | ) { |
| 848 | break; | 831 | break; |
| 849 | } | 832 | } |
| ... | @@ -966,11 +949,7 @@ export function deriveMessagesTimelineRows(input: { | ... | @@ -966,11 +949,7 @@ export function deriveMessagesTimelineRows(input: { |
| 966 | } | 949 | } |
| 967 | 950 | ||
| 968 | if (timelineEntry.kind === "work") { | 951 | if (timelineEntry.kind === "work") { |
| 969 | if ( | 952 | if (timelineEntry.entry.agentSpawn !== undefined || timelineEntry.entry.tone === "error") { |
| 970 | timelineEntry.entry.agentSpawn !== undefined || | ||
| 971 | timelineEntry.entry.tone === "error" || | ||
| 972 | workEntryRendersImagePreview(timelineEntry.entry) | ||
| 973 | ) { | ||
| 974 | nextRows.push({ | 953 | nextRows.push({ |
| 975 | kind: "work", | 954 | kind: "work", |
| 976 | id: timelineEntry.id, | 955 | id: timelineEntry.id, |
| ... | @@ -990,7 +969,6 @@ export function deriveMessagesTimelineRows(input: { | ... | @@ -990,7 +969,6 @@ export function deriveMessagesTimelineRows(input: { |
| 990 | nextEntry.entry.agentSpawn !== undefined || | 969 | nextEntry.entry.agentSpawn !== undefined || |
| 991 | nextEntry.entry.sourceActivityKind === "context-compaction" || | 970 | nextEntry.entry.sourceActivityKind === "context-compaction" || |
| 992 | nextEntry.entry.tone === "error" || | 971 | nextEntry.entry.tone === "error" || |
| 993 | workEntryRendersImagePreview(nextEntry.entry) || | ||
| 994 | activeWorkEntryIds.has(nextEntry.id) || | 972 | activeWorkEntryIds.has(nextEntry.id) || |
| 995 | collapsedEntryIds.has(nextEntry.id) || | 973 | collapsedEntryIds.has(nextEntry.id) || |
| 996 | foldsByAnchorEntryId.has(nextEntry.id) | 974 | foldsByAnchorEntryId.has(nextEntry.id) |
docs/internals/providers.md+12| ... | @@ -24,6 +24,18 @@ adapter in a child scope. Adapter implementations live beside them in | ... | @@ -24,6 +24,18 @@ adapter in a child scope. Adapter implementations live beside them in |
| 24 | [`ProviderAdapter.ts`][adapter]. Read the driver plus its adapter to see how a specific agent's | 24 | [`ProviderAdapter.ts`][adapter]. Read the driver plus its adapter to see how a specific agent's |
| 25 | transport, config, and event shapes are mapped. | 25 | transport, config, and event shapes are mapped. |
| 26 | 26 | ||
| 27 | ## Runtime context | ||
| 28 | |||
| 29 | Every adapter uses `apps/server/src/provider/RuntimeInstructions.ts` to identify T3 Code and | ||
| 30 | the harness, and describe Markdown image/video embeds. Codex includes it in developer | ||
| 31 | instructions; Claude appends it to its system preset; OpenCode sends it in each prompt's | ||
| 32 | `system` field. Cursor, Grok, and Antigravity append a separate text block to ACP prompts, | ||
| 33 | which have no system-message field. This does not change the stored user message. | ||
| 34 | |||
| 35 | Per-turn context includes the current model when known. Codex includes reasoning effort; | ||
| 36 | Grok includes it when explicitly selected for the turn. Claude's session-level context omits model and effort because they can | ||
| 37 | change during a session. OpenCode variants are not assumed to be reasoning-effort levels. | ||
| 38 | |||
| 27 | ## Codex async questions | 39 | ## Codex async questions |
| 28 | 40 | ||
| 29 | Codex 0.153 exposes `request_user_input_async` through `item/started` and `item/completed` | 41 | Codex 0.153 exposes `request_user_input_async` through `item/started` and `item/completed` |