Skip to content

Commit a7e9e59

Browse files
committed
feat(acp): add auth-required dock, elicitation handling, and generic ACP provider UI
- add ThreadAuthRequiredDock and acpRegistryAuth helpers for env-var/agent-owned auth flows - introduce acpGeneric renderer provider and register composer controls - extend ACP session/probe/mapping to support elicitation requests and model descriptions - share questionAnswers decoder across claude, opencode, and acp providers - surface model descriptions as secondary hints in ProviderModelMenu - bump @agentclientprotocol/sdk to ^0.21.1 and harden Windows build script
1 parent 15d7fef commit a7e9e59

44 files changed

Lines changed: 3348 additions & 417 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/_build.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,14 @@ jobs:
108108
run: |
109109
if [ "${{ matrix.os }}" = "windows-latest" ]; then
110110
pnpm run prepare:package-assets
111-
pnpm exec electron-builder --win
111+
pnpm exec electron-builder --config electron-builder.config.cjs --win
112112
else
113113
if [ "${{ matrix.os }}" = "macos-latest" ]; then
114114
pnpm run prepare:agent-plugins
115-
pnpm exec electron-builder --mac
115+
pnpm exec electron-builder --config electron-builder.config.cjs --mac
116116
else
117117
pnpm run prepare:package-assets
118-
pnpm exec electron-builder --linux
118+
pnpm exec electron-builder --config electron-builder.config.cjs --linux
119119
fi
120120
fi
121121
shell: bash

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"prepare": "husky"
5252
},
5353
"dependencies": {
54-
"@agentclientprotocol/sdk": "^0.21.0",
54+
"@agentclientprotocol/sdk": "^0.21.1",
5555
"@anthropic-ai/claude-agent-sdk": "^0.3.142",
5656
"@chenglou/pretext": "^0.0.6",
5757
"@dnd-kit/abstract": "^0.4.0",

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/build-desktop-artifact.mjs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// by glob is a moving target. Instead we copy the build artifacts into a clean
88
// staging directory, write a fresh package.json that lists ONLY the runtime
99
// externals reported by `scripts/scan-runtime-externals.mjs`, run a flat
10-
// `npm install`, and run electron-builder there.
10+
// `pnpm install` (node-linker=hoisted), and run electron-builder there.
1111

1212
import { spawnSync } from "node:child_process";
1313
import {
@@ -106,10 +106,10 @@ function detectHostPlatform() {
106106

107107
const PLATFORM_FLAG = { mac: "--mac", linux: "--linux", win: "--win" };
108108

109-
// npm doesn't auto-install peer deps in every situation, and a missing peer
110-
// of a runtime external surfaces as `ERR_MODULE_NOT_FOUND` deep inside SDK
111-
// code at app launch. Walk each external's installed package.json and pull
112-
// in any non-optional peer that the root itself declares as a dep.
109+
// A missing peer of a runtime external surfaces as `ERR_MODULE_NOT_FOUND` deep
110+
// inside SDK code at app launch. Walk each external's installed package.json
111+
// and pull in any non-optional peer that the root itself declares as a dep —
112+
// this stays explicit regardless of the installer's auto-peer behavior.
113113
function expandPeerDeps(externals, rootPkg) {
114114
const expanded = new Set();
115115
const rootHasDep = (name) =>
@@ -187,7 +187,7 @@ function copyDir(from, to) {
187187
// electron-builder.yml keeps them out of the shipped app; this prune shrinks
188188
// the stage tmpdir and speeds up electron-builder's file walk.
189189
//
190-
// Safe to delete after `npm install` returned: the platform packs are pure
190+
// Safe to delete after `pnpm install` returned: the platform packs are pure
191191
// binary blobs with no postinstall hooks, and the SDK only `require`s them
192192
// lazily at runtime (when `pathToClaudeCodeExecutable` is unset) — which we
193193
// always set on posix and route through wsl.exe on WSL.
@@ -272,15 +272,23 @@ async function main() {
272272
writeFileSync(join(stageRoot, "electron-builder.yml"), buildElectronBuilderConfig());
273273

274274
// 6. Install prod + stage devdeps with a flat layout.
275-
// We DON'T use --omit=optional because electron-builder's dmg-builder
275+
// node-linker=hoisted makes pnpm produce an npm-style flat node_modules,
276+
// which electron-builder's file walker (and our asar globs) expect.
277+
// dangerously-allow-all-builds bypasses pnpm 10+'s build-script gating
278+
// (electron's postinstall must run to download the binary; better-sqlite3
279+
// and node-pty compile native bindings). The stage's deps are pinned to
280+
// versions the root project already trusts, so this is no riskier than
281+
// `pnpm install` on the root.
282+
// We DON'T disable optionals because electron-builder's dmg-builder
276283
// requires the `dmg-license` optionalDependency unconditionally at
277284
// import time. Instead we install optionals normally, then surgically
278285
// delete the only optional we actually want gone: the Claude SDK's
279286
// platform-specific `claude` SEA binary (~200 MB).
280-
// --no-package-lock keeps the stage stateless across runs.
287+
// The stage tmpdir is fresh each run, so any generated pnpm-lock.yaml
288+
// is ephemeral and gets discarded with the stage.
281289
run(
282-
"npm",
283-
["install", "--no-package-lock", "--no-fund", "--no-audit", "--ignore-scripts=false"],
290+
"pnpm",
291+
["install", "--config.node-linker=hoisted", "--config.dangerously-allow-all-builds=true"],
284292
{ cwd: stageRoot },
285293
);
286294

src/renderer/components/common/ProviderModelMenu/ProviderModelMenu.test.tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,81 @@ describe("ProviderModelMenu", () => {
144144
expect(await screen.findByText("Model 500")).toBeInTheDocument();
145145
});
146146

147+
it("renders normalized model rate descriptions as muted row hints", async () => {
148+
const provider = makeProvider(1);
149+
provider.capabilities.models = [
150+
{
151+
id: "opus",
152+
label: "Opus",
153+
description: "2x",
154+
},
155+
];
156+
157+
render(
158+
<ProviderModelMenu
159+
providers={[provider]}
160+
currentAgentKind="codex"
161+
currentModel="opus"
162+
onChange={vi.fn<(next: { agentKind: string; model: string }) => void>()}
163+
/>,
164+
);
165+
166+
fireEvent.click(screen.getByRole("button", { name: "Select model" }));
167+
168+
expect(await screen.findByRole("option", { name: /Opus/u })).toHaveTextContent("· 2x");
169+
});
170+
171+
it("ignores provider prose model descriptions", async () => {
172+
const provider = makeProvider(1);
173+
provider.capabilities.models = [
174+
{
175+
id: "opus",
176+
label: "Opus",
177+
description: "2x Factory token rate",
178+
},
179+
];
180+
181+
render(
182+
<ProviderModelMenu
183+
providers={[provider]}
184+
currentAgentKind="codex"
185+
currentModel="opus"
186+
onChange={vi.fn<(next: { agentKind: string; model: string }) => void>()}
187+
/>,
188+
);
189+
190+
fireEvent.click(screen.getByRole("button", { name: "Select model" }));
191+
192+
expect(await screen.findByRole("option", { name: /Opus/u })).toHaveTextContent("Opus");
193+
expect(screen.getByRole("option", { name: /Opus/u })).not.toHaveTextContent("Factory");
194+
});
195+
196+
it("keeps raw model descriptions available without rendering them in the row", async () => {
197+
const provider = makeProvider(1);
198+
provider.capabilities.models = [
199+
{
200+
id: "opus",
201+
label: "Opus",
202+
description: "2x",
203+
tooltipDescription: "2x Factory token rate",
204+
},
205+
];
206+
207+
render(
208+
<ProviderModelMenu
209+
providers={[provider]}
210+
currentAgentKind="codex"
211+
currentModel="opus"
212+
onChange={vi.fn<(next: { agentKind: string; model: string }) => void>()}
213+
/>,
214+
);
215+
216+
fireEvent.click(screen.getByRole("button", { name: "Select model" }));
217+
const row = await screen.findByRole("option", { name: /Opus/u });
218+
expect(row).toHaveTextContent("· 2x");
219+
expect(screen.queryByText("2x Factory token rate")).not.toBeInTheDocument();
220+
});
221+
147222
it("keeps the current provider header rendered while scrolling deep into a long section", async () => {
148223
render(
149224
<ProviderModelMenu

src/renderer/components/common/ProviderModelMenu/ProviderModelMenu.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const MODEL_MENU_ROW_HEIGHT = 28;
2727
const MODEL_MENU_PROVIDER_HEADER_BOTTOM_GAP = 4;
2828
const MODEL_MENU_MAX_HEIGHT = 288;
2929
const MODEL_MENU_OVERSCAN_ROWS = 16;
30+
const MODEL_DESCRIPTION_TOOLTIP_DELAY_MS = 1000;
3031

3132
interface WindowedItemsMeta {
3233
structureKey: string;
@@ -667,8 +668,8 @@ function WindowedProviderModelList(props: {
667668
// the label string itself (e.g. "GPT-5.5 · 272K · Medium").
668669
// Render the head as the model name and the tail as muted hint.
669670
const { name, hint } = splitModelLabel(item.label);
670-
const mutedHint = hint ?? item.contextDescription;
671-
return (
671+
const mutedHint = [hint, item.contextDescription].filter(Boolean).join(" · ");
672+
const content = (
672673
<span className="flex min-w-0 flex-1 items-center gap-1.5">
673674
<span className="min-w-0 truncate">{name}</span>
674675
{mutedHint ? (
@@ -678,6 +679,19 @@ function WindowedProviderModelList(props: {
678679
) : null}
679680
</span>
680681
);
682+
return item.tooltipDescription ? (
683+
<Tooltip delay={MODEL_DESCRIPTION_TOOLTIP_DELAY_MS}>
684+
{content}
685+
<Tooltip.Content
686+
placement="right"
687+
className="max-w-72 whitespace-normal break-words text-xs"
688+
>
689+
{item.tooltipDescription}
690+
</Tooltip.Content>
691+
</Tooltip>
692+
) : (
693+
content
694+
);
681695
})()}
682696
{item.showProviderIcon || item.subProviderLabel ? (
683697
<span className="ml-auto flex min-w-0 shrink-0 items-center gap-1 text-muted/70">

0 commit comments

Comments
 (0)