Skip to content

feat(copilot): add GitHub Copilot provider (via @github/copilot-sdk) - #7656

Draft
NSExceptional wants to merge 1 commit into
pingdotgg:mainfrom
NSExceptional:github-copilot-provider
Draft

feat(copilot): add GitHub Copilot provider (via @github/copilot-sdk)#7656
NSExceptional wants to merge 1 commit into
pingdotgg:mainfrom
NSExceptional:github-copilot-provider

Conversation

@NSExceptional

@NSExceptional NSExceptional commented Aug 20, 2026

Copy link
Copy Markdown

Draft / RFC. Opening this early for maintainer feedback on integration and conventions. It builds, typechecks, and passes its unit tests, and I've smoke-tested real turns against the live copilot CLI — but I'm not confident it hits every T3 convention (multi-surface, receipts, provider parity), so I'd love guidance on what to tighten before it's merge-ready.

What

Adds GitHub Copilot as a first-class provider, alongside Codex / Claude / Cursor / Grok / OpenCode. It's driven by GitHub's first-party @github/copilot-sdk, which spawns and drives the installed copilot runtime binary over its typed JSON-RPC protocol (RuntimeConnection.forStdio) — the same engine the Copilot CLI/IDE use. No extra runtime download; it reuses whatever copilot the user already has (e.g. Homebrew).

How

  • Provider / driver / adapter under apps/server/src/provider/, plus a small provider/sdk/ layer:
    • CopilotSdkClient — scoped Effect wrapper around CopilotClient (start on acquire, stop on release; one shared client per provider instance).
    • CopilotSdkModels — maps client.listModels() into per-model capabilities: reasoning effort from each model's supportedReasoningEfforts, and a context-window tier gated on the model's longContext billing block. Applied via SessionConfig / session.setModel.
    • CopilotSdkRuntimeEvents — translates SDK SessionEvents into the canonical ProviderRuntimeEvent stream.
  • Session lifecycle: per-thread CopilotSession, a callback→Effect event bridge (SDK is callback-based, unlike the async-iterable providers), permission requests wired into the existing approval flow, and send + session.idle turn handling.
  • Model discovery via client.listModels().
  • Git text generation (commit messages, PR content, branch names, thread titles) uses the SDK's one-shot sendAndWait.
  • Contracts: adds copilot.sdk.event / copilot.sdk.permission runtime sources and Copilot settings/model schemas; web wiring for the provider settings + model picker (reuses the generic option-descriptor UI).
  • Resolves the copilot binary to an absolute path before spawning (a GUI-launched app inherits a minimal PATH), and passes env only on the stdio connection (the SDK rejects env in both places).

Testing

  • tsgo typecheck clean (contracts / server / web); targeted lint clean.
  • Unit tests for the model→capability mapping, tunable resolution, and provider status parsing (74 provider + text-gen tests green).
  • End-to-end smoke test against copilot 1.0.80: createSession → streaming sendsession.idle, setModel with long_context, and the permission callback. listModels() returns per-model reasoning efforts / context tiers as expected.

Notes / open questions

  • Builds on earlier Copilot-provider groundwork by @its-hmny (credited via Co-authored-by); I ported it to the SDK and reworked model discovery + tunables.
  • The SDK is callback-based; I bridge its events into an Effect queue. Happy to align that with how you'd prefer provider adapters to consume SDK streams.
  • Mobile surface and docs aren't touched yet — guidance welcome on what's expected for a new provider.

Authored with Claude Opus 4.8 via Claude Code.

Note

Add GitHub Copilot provider via @github/copilot-sdk

  • Introduces a full copilot provider driver registered in builtInDrivers.ts, wiring together a SDK client, adapter, text generation, and provider snapshot pipeline.
  • CopilotSdkClient.ts wraps the SDK with Effect-based methods, binary path resolution, and scoped lifecycle management.
  • CopilotAdapter.ts manages per-thread sessions, streaming runtime events, permission requests, resume cursors, and in-session model switching.
  • CopilotTextGeneration.ts implements commit message, PR content, branch name, and thread title generation with schema-validated JSON and a 180s timeout.
  • CopilotProvider.ts probes the Copilot CLI for version/auth status and discovers models via the SDK, enriching snapshots asynchronously.
  • Contracts and UI are updated to recognize the copilot kind, default to gpt-4.1 / gpt-4.1-mini, and surface the provider in pickers and settings dialogs.
  • Risk: adds @github/copilot-sdk dependency and disallows koffi builds in pnpm-workspace.yaml; COPILOT_DRIVER_KIND defaults in model.ts may affect model selection if overrides are absent.
📊 Macroscope summarized 56d5f9a. 17 files reviewed, 16 issues evaluated, 2 issues filtered, 10 comments posted

🗂️ Filtered Issues

apps/server/src/provider/Layers/CopilotAdapter.ts — 3 comments posted, 5 evaluated, 1 filtered
  • line 447: The adapter constructs or accepts nativeEventLogger, but never invokes its write method. Therefore configuring nativeEventLogPath or injecting a native logger creates/manages the logger while every Copilot SDK event is silently omitted from the native event log. [ Out of scope (post-validation triage) ]
apps/server/src/provider/Layers/CopilotProvider.ts — 2 comments posted, 3 evaluated, 1 filtered
  • line 115: detectCopilotAuthFromEnvironment uses nullish coalescing before checking whether a token is nonempty. If COPILOT_GITHUB_TOKEN is defined as "" or whitespace while GH_TOKEN or GITHUB_TOKEN contains a valid token, the empty first value wins and the function returns unknown instead of authenticated. Select the first nonblank value rather than the first non-nullish one. [ Out of scope (post-validation triage) ]

@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ca25dc0d-2fde-4c95-bdfc-dd039f0a746f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Aug 20, 2026
Comment thread apps/server/src/provider/Drivers/CopilotDriver.ts Outdated
Comment thread apps/server/src/provider/Layers/CopilotAdapter.ts
Comment thread apps/server/src/provider/Layers/CopilotAdapter.ts
Comment thread apps/server/src/provider/Layers/CopilotAdapter.ts Outdated
Comment thread packages/contracts/src/settings.ts
Comment thread apps/server/src/provider/Layers/CopilotProvider.ts Outdated
turnId: input.turnId,
itemId: RuntimeItemId.make(input.data.toolCallId),
payload: {
itemType: "dynamic_tool_call",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium sdk/CopilotSdkRuntimeEvents.ts:205

makeSdkToolCompleteEvent emits itemType: "dynamic_tool_call" for every completed tool, so the lifecycle collapse overwrites the start event's command_execution, file_change, or mcp_tool_call classification and renders completed tools as generic tools. Preserve the classification for each toolCallId and reuse it when emitting progress and completion events.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/sdk/CopilotSdkRuntimeEvents.ts around line 205:

`makeSdkToolCompleteEvent` emits `itemType: "dynamic_tool_call"` for every completed tool, so the lifecycle collapse overwrites the start event's `command_execution`, `file_change`, or `mcp_tool_call` classification and renders completed tools as generic tools. Preserve the classification for each `toolCallId` and reuse it when emitting progress and completion events.

const generateThreadTitle: TextGenerationShape["generateThreadTitle"] = Effect.fn(
"CopilotTextGeneration.generateThreadTitle",
)(function* (input) {
const { prompt, outputSchema } = buildThreadTitlePrompt({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium textGeneration/CopilotTextGeneration.ts:202

The Copilot provider ignores configured generation context: generateThreadTitle drops previousTitle, while generateBranchName, generatePrContent, and generateCommitMessage drop their respective policy values; generatePrContent also drops changeRequestTemplate. As a result, title regeneration uses the initial-title prompt, and branch names, PR content, and commit messages are generated without the caller's naming, template, or instruction constraints. Pass these omitted fields through to the corresponding build*Prompt calls.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/textGeneration/CopilotTextGeneration.ts around line 202:

The Copilot provider ignores configured generation context: `generateThreadTitle` drops `previousTitle`, while `generateBranchName`, `generatePrContent`, and `generateCommitMessage` drop their respective `policy` values; `generatePrContent` also drops `changeRequestTemplate`. As a result, title regeneration uses the initial-title prompt, and branch names, PR content, and commit messages are generated without the caller's naming, template, or instruction constraints. Pass these omitted fields through to the corresponding `build*Prompt` calls.

const { settings, snapshot, publishSnapshot } = input;
const stampIdentity = input.stampIdentity ?? ((value) => value);

const enrichVersionAdvisory = enrichProviderSnapshotWithVersionAdvisory(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Layers/CopilotProvider.ts:349

enrichCopilotSnapshot ignores the user's provider update-check setting, so Copilot still requests the latest version from the npm registry and publishes version advisories when checks are disabled. Add enableProviderUpdateChecks to this function's input, pass it as the options argument to enrichProviderSnapshotWithVersionAdvisory, and forward the setting from the caller.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CopilotProvider.ts around line 349:

`enrichCopilotSnapshot` ignores the user's provider update-check setting, so Copilot still requests the latest version from the npm registry and publishes version advisories when checks are disabled. Add `enableProviderUpdateChecks` to this function's input, pass it as the `options` argument to `enrichProviderSnapshotWithVersionAdvisory`, and forward the setting from the caller.

Comment on lines +107 to +113
const seen = new Set<string>();
for (const dir of [...pathDirs, ...commonDirs]) {
if (!dir || seen.has(dir)) continue;
seen.add(dir);
const candidate = NodePath.join(dir, binary);
if (await isExecutable(candidate)) return candidate;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High sdk/CopilotSdkClient.ts:107

On Windows, resolveBinaryPath fails to resolve a normal npm-installed copilot.cmd, so it passes bare copilot to RuntimeConnection.forStdio and SDK-backed Copilot operations fail even though the CLI is installed. The probe only checks NodePath.join(dir, binary); probe the PATHEXT suffixes (including .CMD) before falling back to the bare name.

Suggested change
const seen = new Set<string>();
for (const dir of [...pathDirs, ...commonDirs]) {
if (!dir || seen.has(dir)) continue;
seen.add(dir);
const candidate = NodePath.join(dir, binary);
if (await isExecutable(candidate)) return candidate;
}
const extensions =
process.platform === "win32"
? (env?.PATHEXT || process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";")
: [""];
const seen = new Set<string>();
for (const dir of [...pathDirs, ...commonDirs]) {
if (!dir || seen.has(dir)) continue;
seen.add(dir);
for (const extension of extensions) {
const candidate = NodePath.join(dir, `${binary}${extension}`);
if (await isExecutable(candidate)) return candidate;
}
}
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/sdk/CopilotSdkClient.ts around lines 107-113:

On Windows, `resolveBinaryPath` fails to resolve a normal npm-installed `copilot.cmd`, so it passes bare `copilot` to `RuntimeConnection.forStdio` and SDK-backed Copilot operations fail even though the CLI is installed. The probe only checks `NodePath.join(dir, binary)`; probe the `PATHEXT` suffixes (including `.CMD`) before falling back to the bare name.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effect Service Conventions — 6 findings

The new Copilot provider code mostly follows the repo's Effect conventions, but a few error-handling details in the new files diverge from them (and from the sibling Grok/Cursor/Aether implementations):

  • CopilotSdkError is a Data.TaggedError whose only payload is a stringified cause.
  • CopilotTextGeneration hand-rolls a _tag predicate, adds a pass-through error factory, and uses Effect.catchTag instead of Effect.catchTags.
  • CopilotDriver / CopilotAdapter fold cause.message into the caller-visible detail.

Details inline.

Posted via Macroscope — Effect Service Conventions

Comment on lines +163 to +160
new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail: `Failed to build Copilot snapshot: ${cause.message ?? String(cause)}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProviderDriverError.message is built from detail, so interpolating cause.message here derives the caller-visible message from the cause. Keep detail a stable structural phrase and let cause carry the dynamic part (same as AetherDriver / OpenCode2Driver).

Suggested change
detail: `Failed to build Copilot snapshot: ${cause.message ?? String(cause)}`,
detail: "Failed to build the Copilot provider snapshot.",

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/provider/sdk/CopilotSdkClient.ts Outdated
Comment thread apps/server/src/textGeneration/CopilotTextGeneration.ts Outdated
Comment thread apps/server/src/textGeneration/CopilotTextGeneration.ts Outdated
Comment thread apps/server/src/provider/Layers/CopilotAdapter.ts Outdated
Comment thread apps/server/src/textGeneration/CopilotTextGeneration.ts
@NSExceptional
NSExceptional force-pushed the github-copilot-provider branch from 56d5f9a to c10b8ef Compare August 20, 2026 09:40
@NSExceptional

Copy link
Copy Markdown
Author

Thanks for the automated review — addressed the findings (force-pushed c10b8ef). Typecheck + lint clean, 75 provider/text-gen tests green.

Correctness

  • CopilotAdapter consumer fiber (High) — forked with Effect.forkChild, so it was tied to the startSession fiber and interrupted on return. Now Effect.forkIn(adapterScope); torn down explicitly in stopSessionInternal.
  • Concurrent sendTurn (High) — a second turn overwrote activeTurnCompletion and misattributed events. Now rejects a second in-flight turn (A turn is already in progress) rather than serialize across the whole turn, since stopSession/interruptTurn must run mid-turn.
  • ServerSettingsPatch missing copilot (High) — added CopilotSettingsPatch and registered it in the patch provider map, so binaryPath/enabled persist.
  • Stale activeTurnId (Medium) — cleared both ctx.activeTurnId and the session's activeTurnId on completion so listSessions() doesn't report an idle session as active.
  • refreshInterval override (Medium) — removed; Copilot now follows providerHealthRefreshInterval like the other providers.
  • parseCopilotVersionOutput missing-binary (Medium) — the nonzero-exit branch now also matches not found/enoent, so the install guidance is surfaced (added a test).

Effect conventions

  • CopilotTextGeneration now uses Schema.is(TextGenerationError) and Effect.catchTags, and drops the pass-through error factory (matches CursorTextGeneration).
  • CopilotSdkError aligned to { operation, detail, cause } like OpenCodeRuntimeError; adapter errors use a static detail + cause instead of folding cause.message.

One I left as-is: the Failed to build Copilot snapshot: ${cause.message} detail in CopilotDriver — that mirrors GrokDriver's exact pattern, so I kept it consistent with the sibling. Happy to change if you'd prefer otherwise.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One outstanding convention issue remains in the Copilot SDK error-wrapping path. Earlier findings on CopilotTextGeneration.ts (redundant error factory, hand-rolled _tag predicate, catchTag) are addressed. The still-open threads on CopilotDriver.ts:160 and CopilotSdkClient.ts:32-45 also remain applicable.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/provider/Layers/CopilotAdapter.ts Outdated
Comment thread apps/server/src/provider/sdk/CopilotSdkModels.ts Outdated
Comment thread apps/server/src/provider/Layers/CopilotAdapter.ts
@NSExceptional
NSExceptional force-pushed the github-copilot-provider branch from c10b8ef to 45ca7b9 Compare August 20, 2026 09:46
@NSExceptional

Copy link
Copy Markdown
Author

Second pass on the re-review (45ca7b9):

  • Native event logging — the nativeEventLogger was accepted but never fed. Added a logNative helper (mirroring GrokAdapter) and now record each SDK session event and permission request/completion, so nativeEventLogPath / the injected logger capture Copilot traffic.
  • resolveCopilotSdkTunables validation — now validates reasoning effort against the allowed set (none/low/medium/high/xhigh/max) and drops boolean/invalid values, matching the context-tier path and the documented guarantee (added tests).
  • Adapter error details — the getClient / createSession mappings now use stable structural phrases ("Failed to start the Copilot SDK runtime client." / "Failed to create or resume the Copilot SDK session.") with the raw cause preserved, instead of copying the SDK failure string.

Typecheck + lint clean, 76 tests green.

Comment thread apps/server/src/provider/Layers/CopilotAdapter.ts
makeProviderMaintenanceCapabilities({
provider: DRIVER_KIND,
packageName: null,
updateExecutable: "copilot",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Drivers/CopilotDriver.ts:53

The update action ignores an instance's configured binaryPath, so custom or absolute Copilot installations are updated using the PATH-resolved copilot binary—or fail when copilot is not on PATH. makeStaticProviderMaintenanceResolver always returns the hard-coded executable from UPDATE; use a resolver that derives the update executable from the supplied binaryPath.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Drivers/CopilotDriver.ts around line 53:

The update action ignores an instance's configured `binaryPath`, so custom or absolute Copilot installations are updated using the PATH-resolved `copilot` binary—or fail when `copilot` is not on `PATH`. `makeStaticProviderMaintenanceResolver` always returns the hard-coded executable from `UPDATE`; use a resolver that derives the update executable from the supplied `binaryPath`.

Comment thread apps/server/src/provider/Layers/CopilotAdapter.ts Outdated
Comment thread apps/server/src/provider/Layers/CopilotProvider.ts Outdated
@NSExceptional

NSExceptional commented Aug 20, 2026

Copy link
Copy Markdown
Author

Round 3 (1e7bc5722):

  • interruptTurn could strand a turn (High) — if abort() rejected, no session.idle arrived and sendTurn blocked forever. Now completes the active turn as aborted after abort() settles (idempotent, so a normal abort that emits session.idle just no-ops).
  • send rejection left a stale active turn (Medium) — the error path now clears activeTurnId and session.activeTurnId too, not just activeTurnCompletion.
  • Auth env precedence (Medium) — a blank COPILOT_GITHUB_TOKEN no longer masks a real GH_TOKEN/GITHUB_TOKEN; picks the first non-blank token in order (with a test).

Not changed — flagging as consistent-with-siblings rather than a Copilot-specific bug:

  • Update executable ignores binaryPath (CopilotDriver:53)makeStaticProviderMaintenanceResolver hard-codes the executable, but GrokDriver and ClaudeDriver do exactly the same (grok/claude). Deriving the update binary from a custom binaryPath looks like a repo-wide maintenance-resolver change rather than something to fix only for Copilot — happy to do it separately if you'd like it repo-wide.

Typecheck + lint clean, 77 tests green.

@NSExceptional
NSExceptional force-pushed the github-copilot-provider branch from 45ca7b9 to 1e7bc57 Compare August 20, 2026 09:51
Comment thread apps/server/src/provider/Layers/CopilotAdapter.ts Outdated
@NSExceptional

Copy link
Copy Markdown
Author

Round 4 (a91e1c58b): turn cleanup is now interruption-safe — moved the active-turn reset (activeTurnCompletion / activeTurnId / session.activeTurnId) into an Effect.ensuring finalizer around the send + Deferred.await. Previously an interrupted or failed sendTurn left activeTurnCompletion set, wedging the session (all later turns rejected as already-in-progress). Now it clears on success, failure, or interruption alike. Typecheck + lint clean, 533 provider tests green.

@NSExceptional
NSExceptional force-pushed the github-copilot-provider branch from 1e7bc57 to a91e1c5 Compare August 20, 2026 09:57

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One retained convention issue in the new Copilot SDK layer: CopilotSdkError is still modeled with Data.TaggedError and its detail is a stringified cause. See the inline note. (The earlier findings in CopilotTextGeneration.ts and CopilotAdapter.ts look addressed.)

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/provider/sdk/CopilotSdkClient.ts Outdated
@NSExceptional

Copy link
Copy Markdown
Author

Round 5 (6d40d6ef5): CopilotSdkError now follows the provider/Errors.ts conventionSchema.TaggedErrorClass with cause: Schema.Defect() and a message derived from structural attributes (operation + detail), matching ProviderAdapterRequestError et al. (was Data.TaggedError). Typecheck + lint clean, 77 tests green.

I believe that leaves only the CopilotDriver:53 update-executable note, which I've kept consistent with GrokDriver/ClaudeDriver (all static-executable resolvers) rather than diverging one provider — flagged above as a repo-wide follow-up if you want it.

@NSExceptional
NSExceptional force-pushed the github-copilot-provider branch from a91e1c5 to 6d40d6e Compare August 20, 2026 10:02
Comment thread apps/server/src/provider/sdk/CopilotSdkRuntimeEvents.ts Outdated
@NSExceptional

Copy link
Copy Markdown
Author

Round 6 (6cd5f28b6): tool calls now emit the canonical item.started on tool.execution_start (was item.updated), so consumers see the start of the lifecycle; progress → item.updated, completion → item.completed.

@NSExceptional
NSExceptional force-pushed the github-copilot-provider branch from 6d40d6e to 6cd5f28 Compare August 20, 2026 10:07
});
}
const nextLength = Math.max(0, ctx.turns.length - numTurns);
ctx.turns.splice(nextLength);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Layers/CopilotAdapter.ts:826

rollbackThread reports success after only truncating local ctx.turns, while subsequent ctx.sdkSession.send calls still use the SDK conversation containing the rolled-back turns. This makes regenerated answers retain context the user asked to remove; if provider-side rollback is unavailable, return an explicit unsupported-operation error instead of claiming success.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CopilotAdapter.ts around line 826:

`rollbackThread` reports success after only truncating local `ctx.turns`, while subsequent `ctx.sdkSession.send` calls still use the SDK conversation containing the rolled-back turns. This makes regenerated answers retain context the user asked to remove; if provider-side rollback is unavailable, return an explicit unsupported-operation error instead of claiming success.

if (/(write|edit|str_replace|create_file|apply_patch|patch|delete|move|rename)/.test(name)) {
return "file_change";
}
if (/(search|grep|find|fetch|web|glob)/.test(name)) return "web_search";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium sdk/CopilotSdkRuntimeEvents.ts:55

Repository/code-search tools whose names contain grep, find, or glob are emitted as web_search timeline items, mislabeling ordinary local searches. The matcher treats those generic filesystem/search tokens as web searches; remove them so only web-specific tool names use web_search and the others remain dynamic_tool_call.

Suggested change
if (/(search|grep|find|fetch|web|glob)/.test(name)) return "web_search";
if (/(search|fetch|web)/.test(name)) return "web_search";
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/sdk/CopilotSdkRuntimeEvents.ts around line 55:

Repository/code-search tools whose names contain `grep`, `find`, or `glob` are emitted as `web_search` timeline items, mislabeling ordinary local searches. The matcher treats those generic filesystem/search tokens as web searches; remove them so only web-specific tool names use `web_search` and the others remain `dynamic_tool_call`.

Effect.gen(function* () {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const hostPlatform = yield* HostProcessPlatform;
const command = ChildProcess.make(copilotSettings.binaryPath, ["version"], {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Layers/CopilotProvider.ts:252

The health probe reports copilot as missing when a GUI-launched process has a restricted PATH, even though the SDK can locate the CLI in its usual install locations. runCopilotVersionCommand passes copilotSettings.binaryPath directly to ChildProcess.make instead of using the SDK's binary resolver, so the status check marks an installed provider as errored; resolve the binary before constructing the version command.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CopilotProvider.ts around line 252:

The health probe reports `copilot` as missing when a GUI-launched process has a restricted `PATH`, even though the SDK can locate the CLI in its usual install locations. `runCopilotVersionCommand` passes `copilotSettings.binaryPath` directly to `ChildProcess.make` instead of using the SDK's binary resolver, so the status check marks an installed provider as errored; resolve the binary before constructing the version command.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One retained convention issue in apps/server/src/provider/sdk/CopilotSdkClient.ts: the wrapper's message is still derived from a detail field that copies cause.message. The previously flagged detail interpolation in apps/server/src/provider/Drivers/CopilotDriver.ts:160 is also still open (existing comment).

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/provider/sdk/CopilotSdkClient.ts

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UI consistency review of the changed web files (providerIconUtils.ts, AddProviderInstanceDialog.tsx, providerDriverMeta.ts, session-logic.ts).

Provider registration is consistent: the new copilot driver is added to PROVIDER_CLIENT_DEFINITIONS, PROVIDER_ICON_BY_PROVIDER and PROVIDER_OPTIONS, the stale githubCopilot "Coming Soon" entry is removed alongside its now-unused icon import, and GithubCopilotIcon uses the same fill-black dark:fill-white treatment as the other brand icons, so light/dark tone matches the existing provider rows. No primitive reconstruction, class-override, or CSS-ownership issues found.

One finding: unrelated re-wrapping in providerDriverMeta.ts (details inline).

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/settings/providerDriverMeta.ts Outdated
Comment thread apps/web/src/components/settings/providerDriverMeta.ts Outdated
Adds GitHub Copilot as a first-class provider, driven by the first-party
`@github/copilot-sdk`. The SDK spawns and drives the installed `copilot`
runtime binary over its typed JSON-RPC protocol (`RuntimeConnection.forStdio`),
so no extra runtime download is required.

Highlights:
- Provider / driver / adapter under `apps/server/src/provider/` plus a small
  `provider/sdk/` layer:
  - `CopilotSdkClient` — scoped Effect wrapper around `CopilotClient`.
  - `CopilotSdkModels` — maps `client.listModels()` to per-model capabilities:
    reasoning effort from each model's `supportedReasoningEfforts`, and a
    context-window tier gated on the model's `longContext` billing block.
  - `CopilotSdkRuntimeEvents` — translates SDK `SessionEvent`s into the
    canonical `ProviderRuntimeEvent` stream.
- Session lifecycle: one shared client, per-thread `CopilotSession`, a
  callback→Effect event bridge, permission requests wired into the existing
  approval flow, and `send` + `session.idle` turn handling. Reasoning effort
  and context tier are applied via `SessionConfig` / `session.setModel`.
- Model discovery via `client.listModels()`.
- Git text generation (commit messages, PR content, branch names, thread
  titles) uses the SDK's one-shot `sendAndWait`.
- Contracts: `copilot.sdk.event` / `copilot.sdk.permission` runtime sources and
  Copilot settings/model schemas; web UI wiring for settings + model picker.
- Resolves the `copilot` binary to an absolute path before spawning (a GUI app
  inherits a minimal PATH), and passes env only on the stdio connection.

Co-authored-by: its-hmny <enea.guidi@n26.com>
@NSExceptional

Copy link
Copy Markdown
Author

Round 7 (525c76bda):

  • CopilotSdkError — dropped the detail field; message is now derived purely from the structural operation attribute, with the real failure preserved on cause (matches the provider/Errors.ts models).
  • Formatting — ran vp fmt across all touched files; the earlier ~80-col re-wraps in providerDriverMeta.ts, model.ts, settings.ts, builtInDrivers.ts, etc. are back to the 100-col default, so fmt:check is clean.

@NSExceptional
NSExceptional force-pushed the github-copilot-provider branch from 6cd5f28 to 525c76b Compare August 20, 2026 10:12
detail: `Unknown pending approval request: ${requestId}`,
});
}
yield* Effect.sync(() => pending.resolve(decision));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Layers/CopilotAdapter.ts:792

A second respondToRequest call can find the same pending request and return success even though its decision is discarded by the already-resolved promise. Delete the entry from ctx.pendingApprovals before calling pending.resolve(decision) so the request is consumed atomically.

Suggested change
yield* Effect.sync(() => pending.resolve(decision));
yield* Effect.sync(() => {
ctx.pendingApprovals.delete(requestId);
pending.resolve(decision);
});
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CopilotAdapter.ts around line 792:

A second `respondToRequest` call can find the same pending request and return success even though its decision is discarded by the already-resolved promise. Delete the entry from `ctx.pendingApprovals` before calling `pending.resolve(decision)` so the request is consumed atomically.

Comment on lines +147 to +155
const client = new CopilotClient({
// Env goes ONLY on the stdio connection — the SDK rejects setting it at
// both the client level and the connection level, and prefers the
// connection-level env for child-process transports.
connection: RuntimeConnection.forStdio({ path, ...(env ? { env } : {}) }),
...(input.logLevel ? { logLevel: input.logLevel } : {}),
});
await client.start();
return client;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High sdk/CopilotSdkClient.ts:147

Failed client.start() calls leak the spawned Copilot process, so repeated startup failures can exhaust child-process and system resources. Because start() rejects before Effect.acquireRelease receives client, the release action is never registered; catch the failure, stop (or force-stop) the client, then rethrow it.

       const client = new CopilotClient({
@@
-      await client.start();
+      try {
+        await client.start();
+      } catch (error) {
+        await client.stop().catch(() => client.forceStop());
+        throw error;
+      }
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/sdk/CopilotSdkClient.ts around lines 147-155:

Failed `client.start()` calls leak the spawned Copilot process, so repeated startup failures can exhaust child-process and system resources. Because `start()` rejects before `Effect.acquireRelease` receives `client`, the release action is never registered; catch the failure, stop (or force-stop) the client, then rethrow it.

const turnModelSelection =
input.modelSelection?.instanceId === boundInstanceId ? input.modelSelection : undefined;
const model = turnModelSelection?.model ?? ctx.session.model;
const tunables = resolveCopilotSdkTunables(turnModelSelection?.options);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Layers/CopilotAdapter.ts:662

A turn that omits modelSelection clears the reasoning-effort and context-tier settings applied at session start. resolveCopilotSdkTunables(undefined) returns {}, so applyModelSelection detects a change and calls setModel without those tunables; preserve ctx's applied tunables when no per-turn selection is supplied.

-        const tunables = resolveCopilotSdkTunables(turnModelSelection?.options);
+        const tunables = turnModelSelection
+          ? resolveCopilotSdkTunables(turnModelSelection.options)
+          : {
+              reasoningEffort:
+                ctx.appliedReasoningEffort as CopilotSdkSessionTunables["reasoningEffort"],
+              contextTier: ctx.appliedContextTier as CopilotSdkSessionTunables["contextTier"],
+            };
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/CopilotAdapter.ts around line 662:

A turn that omits `modelSelection` clears the reasoning-effort and context-tier settings applied at session start. `resolveCopilotSdkTunables(undefined)` returns `{}`, so `applyModelSelection` detects a change and calls `setModel` without those tunables; preserve `ctx`'s applied tunables when no per-turn selection is supplied.

@NSExceptional

Copy link
Copy Markdown
Author

All three Macroscope review checks are green now (Correctness, Effect Service Conventions, UI Consistency). The only remaining red check is Vercel – t3code-marketing ("Authorization required to deploy") — that's the org deploy gate for an external-fork PR, not something in this diff. Ready for a human look whenever you have a moment; happy to keep iterating on anything else.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant