Skip to content

Commit 5dca2ec

Browse files
committed
ship: address review feedback
1 parent 6f9b7f9 commit 5dca2ec

32 files changed

Lines changed: 400 additions & 189 deletions

apps/desktop/native/ios-sim-helpers/build.sh

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,12 @@ if [[ ! -x "$CAPTURE" || ! -x "$INPUT" ]]; then
8383
fi
8484

8585
if command -v codesign >/dev/null 2>&1; then
86-
codesign --force --sign - "$CAPTURE" >/dev/null 2>&1 || true
87-
codesign --force --sign - "$INPUT" >/dev/null 2>&1 || true
86+
if ! codesign --force --sign - "$CAPTURE" >/dev/null 2>&1; then
87+
echo "warning: failed to ad-hoc sign $CAPTURE" >&2
88+
fi
89+
if ! codesign --force --sign - "$INPUT" >/dev/null 2>&1; then
90+
echo "warning: failed to ad-hoc sign $INPUT" >&2
91+
fi
8892
fi
8993

9094
if [[ "$SMOKE" == "1" ]]; then

apps/desktop/package.json

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -186,16 +186,6 @@
186186
{
187187
"from": "scripts/ade-cli-install-path.cmd",
188188
"to": "ade-cli/install-path.cmd"
189-
},
190-
{
191-
"from": "native/ios-sim-helpers",
192-
"to": "native/ios-sim-helpers",
193-
"filter": [
194-
"README.md",
195-
"build.sh",
196-
"sim-capture.swift",
197-
"sim-input.m"
198-
]
199189
}
200190
],
201191
"afterPack": "./scripts/after-pack-runtime-fixes.cjs",
@@ -238,7 +228,19 @@
238228
"entitlementsInherit": "build/entitlements.mac.inherit.plist",
239229
"notarize": true,
240230
"mergeASARs": false,
241-
"x64ArchFiles": "Contents/Resources/app.asar.unpacked/{node_modules,vendor}/**/*"
231+
"x64ArchFiles": "Contents/Resources/app.asar.unpacked/{node_modules,vendor}/**/*",
232+
"extraResources": [
233+
{
234+
"from": "native/ios-sim-helpers",
235+
"to": "native/ios-sim-helpers",
236+
"filter": [
237+
"README.md",
238+
"build.sh",
239+
"sim-capture.swift",
240+
"sim-input.m"
241+
]
242+
}
243+
]
242244
}
243245
}
244246
}

apps/desktop/scripts/audit-chat-model-runtime.mjs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1057,7 +1057,9 @@ async function main() {
10571057
results = await runSmokeCases(client, cases, lane.id, options);
10581058
}
10591059

1060-
const ok = results.every((result) => result.ok);
1060+
const ok = options.mode === "list"
1061+
? true
1062+
: models.length > 0 && cases.length > 0 && results.length > 0 && results.every((result) => result.ok);
10611063
const payload = {
10621064
ok,
10631065
mode: options.mode,

apps/desktop/scripts/dev.cjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,9 @@ async function main() {
208208
if (!Number.isFinite(remoteDebugPort) || remoteDebugPort <= 0 || remoteDebugPort > 65535) {
209209
throw new Error(`Invalid Electron remote debugging port: ${remoteDebugPortRaw}`);
210210
}
211+
if (!(await isPortFree(remoteDebugPort))) {
212+
throw new Error(`Electron remote debugging port ${remoteDebugPort} is already in use.`);
213+
}
211214
if (process.env.ADE_APP_CONTROL_CDP_PORT) {
212215
process.stdout.write(`[ade] honoring ADE App Control CDP port ${remoteDebugPort}\n`);
213216
}

apps/desktop/src/main/services/ai/aiIntegrationService.test.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -479,11 +479,13 @@ describe("aiIntegrationService", () => {
479479
verifiedAt: "2026-03-17T19:00:00.000Z",
480480
});
481481

482-
await expect(service.verifyApiKeyConnection("cursor")).resolves.toMatchObject({
482+
const result = await service.verifyApiKeyConnection("cursor");
483+
expect(result).toMatchObject({
483484
provider: "cursor",
484485
ok: true,
485486
source: "store",
486487
});
488+
expect(JSON.stringify(result)).not.toContain("crsr_test");
487489

488490
const refreshedStatus = await service.getStatus();
489491
expect(mockState.detectAllAuth.mock.calls.length).toBeGreaterThanOrEqual(3);
@@ -521,10 +523,44 @@ describe("aiIntegrationService", () => {
521523
errorMessage: "Cursor model API returned HTTP 401.",
522524
});
523525

524-
await expect(service.verifyApiKeyConnection("cursor")).resolves.toMatchObject({
526+
const result = await service.verifyApiKeyConnection("cursor");
527+
expect(result).toMatchObject({
525528
provider: "cursor",
526529
ok: false,
527530
source: "store",
528531
});
532+
expect(JSON.stringify(result)).not.toContain("crsr_test");
533+
});
534+
535+
it("does not fail Cursor verification on non-auth model probe failures", async () => {
536+
const { service } = makeService({
537+
providerMode: "guest",
538+
availability: { claude: false, codex: false, cursor: false, droid: false },
539+
});
540+
541+
mockState.detectAllAuth.mockResolvedValue([
542+
{ type: "api-key", provider: "cursor", key: "crsr_test", source: "store" },
543+
]);
544+
mockState.verifyProviderApiKey.mockResolvedValue({
545+
provider: "cursor",
546+
ok: true,
547+
message: "Connection verified successfully.",
548+
endpoint: "Cursor.me",
549+
statusCode: null,
550+
verifiedAt: "2026-03-17T19:00:00.000Z",
551+
});
552+
mockState.probeCursorSdkModelDiscovery.mockResolvedValue({
553+
rows: [],
554+
failureKind: "unavailable",
555+
errorMessage: "Cursor model API returned HTTP 503.",
556+
});
557+
558+
const result = await service.verifyApiKeyConnection("cursor");
559+
expect(result).toMatchObject({
560+
provider: "cursor",
561+
ok: true,
562+
source: "store",
563+
});
564+
expect(JSON.stringify(result)).not.toContain("crsr_test");
529565
});
530566
});

apps/desktop/src/main/services/ai/aiIntegrationService.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -970,15 +970,21 @@ export function createAiIntegrationService(args: {
970970
if (providerName === "cursor" && verification.ok) {
971971
invalidateProviderReadinessCaches();
972972
invalidateInFinally = false;
973-
const discovery = await probeCursorSdkModelDiscovery(apiEntry.key, { timeoutMs: 3_000 });
974-
if (discovery.failureKind === "auth") {
975-
return {
976-
...verification,
977-
ok: false,
978-
message:
979-
"Cursor account verification succeeded, but Cursor rejected this API key for agent/model access. Re-enter a key from the Cursor dashboard integrations page.",
980-
source: apiEntry.source,
981-
};
973+
try {
974+
const discovery = await probeCursorSdkModelDiscovery(apiEntry.key, { timeoutMs: 3_000 });
975+
if (discovery.failureKind === "auth") {
976+
return {
977+
...verification,
978+
ok: false,
979+
message:
980+
"Cursor account verification succeeded, but Cursor rejected this API key for agent/model access. Re-enter a key from the Cursor dashboard integrations page.",
981+
source: apiEntry.source,
982+
};
983+
}
984+
} catch (error) {
985+
logger.warn("ai.cursor.discovery_probe_failed", {
986+
error: error instanceof Error ? error.message : String(error),
987+
});
982988
}
983989
}
984990
return {

apps/desktop/src/main/services/ai/apiKeyStore.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ function writeMacosKeychainSecret(account: string, value: string): void {
163163
if (!isMacosKeychainAvailable()) {
164164
throw new Error("macOS Keychain is unavailable. Cannot persist API keys.");
165165
}
166+
// `/usr/bin/security add-generic-password` does not support a non-interactive
167+
// stdin password sentinel: `-w -` stores a literal dash. Keep this path
168+
// synchronous and tightly scoped until we replace it with a native binding.
166169
const result = runSecurity([
167170
"add-generic-password",
168171
"-a",
@@ -329,13 +332,13 @@ function ensureStore(): StoredKeys {
329332
return cache;
330333
}
331334

332-
function persistEncryptedStore(): void {
333-
if (!storePath || !cache) return;
335+
function persistEncryptedStore(nextStore: StoredKeys = cache ?? {}): void {
336+
if (!storePath) return;
334337
if (!isSecureStorageAvailable()) {
335338
throw new Error("OS secure storage is unavailable. Cannot persist API keys.");
336339
}
337340
fs.mkdirSync(path.dirname(storePath), { recursive: true });
338-
const encrypted = safeStorage!.encryptString(JSON.stringify(cache));
341+
const encrypted = safeStorage!.encryptString(JSON.stringify(nextStore));
339342
fs.writeFileSync(storePath, encrypted);
340343
try {
341344
fs.chmodSync(storePath, 0o600);
@@ -385,14 +388,16 @@ export function storeApiKey(provider: string, key: string): void {
385388
throw new Error("Provider and key are required.");
386389
}
387390
const store = ensureStore();
388-
store[normalizedProvider] = normalizedKey;
389391
if (isMacosKeychainAvailable()) {
390392
writeMacosKeychainSecret(normalizedProvider, normalizedKey);
391393
const index = readMacosKeychainProviderIndex();
392394
writeMacosKeychainProviderIndex(new Set([...index.providers, normalizedProvider]));
395+
store[normalizedProvider] = normalizedKey;
393396
return;
394397
}
395-
persistEncryptedStore();
398+
const nextStore = { ...store, [normalizedProvider]: normalizedKey };
399+
persistEncryptedStore(nextStore);
400+
cache = nextStore;
396401
}
397402

398403
export function getApiKey(provider: string): string | null {
@@ -413,14 +418,17 @@ export function deleteApiKey(provider: string): void {
413418
const normalizedProvider = provider.trim().toLowerCase();
414419
if (!normalizedProvider.length) return;
415420
const store = ensureStore();
416-
delete store[normalizedProvider];
417421
if (isMacosKeychainAvailable()) {
418422
deleteMacosKeychainSecret(normalizedProvider);
419423
const index = readMacosKeychainProviderIndex();
420424
writeMacosKeychainProviderIndex(index.providers.filter((entry) => entry !== normalizedProvider));
425+
delete store[normalizedProvider];
421426
return;
422427
}
423-
persistEncryptedStore();
428+
const nextStore = { ...store };
429+
delete nextStore[normalizedProvider];
430+
persistEncryptedStore(nextStore);
431+
cache = nextStore;
424432
}
425433

426434
export function listStoredProviders(): string[] {

apps/desktop/src/main/services/ai/authDetector.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ import { getLocalProviderDefaultEndpoint, type LocalProviderFamily } from "../..
1515
import type { AiLocalProviderConfigs } from "../../../shared/types";
1616
import { inspectLocalProvider, clearLocalProviderInspectionCache } from "./localModelDiscovery";
1717
import { resolveDroidExecutable } from "./droidExecutable";
18-
import { reportProviderRuntimeAuthFailure } from "./providerRuntimeHealth";
18+
import {
19+
reportProviderRuntimeAuthFailure,
20+
reportProviderRuntimeReady,
21+
} from "./providerRuntimeHealth";
1922

2023
type CliName = "claude" | "codex" | "cursor" | "droid";
2124

@@ -804,6 +807,7 @@ async function verifyCursorApiKey(
804807
);
805808
}),
806809
]);
810+
reportProviderRuntimeReady("cursor");
807811
return {
808812
provider: "cursor",
809813
ok: true,

apps/desktop/src/main/services/ai/providerConnectionStatus.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,15 @@ export async function buildProviderConnections(
9494
if (!health) return;
9595
if (health.state === "auth-failed" || health.state === "runtime-failed") {
9696
status.runtimeAvailable = false;
97+
status.usageAvailable = false;
9798
status.blocker = health.message
9899
?? (health.state === "auth-failed"
99100
? `${status.provider} runtime was detected, but ADE chat reported that login is still required.`
100101
: `${status.provider} runtime was detected, but ADE could not launch it from this app session.`);
101102
} else if (health.state === "ready") {
102103
status.runtimeAvailable = true;
103104
status.authAvailable = true;
105+
if (status.provider === "cursor") status.usageAvailable = true;
104106
status.blocker = null;
105107
}
106108
}

apps/desktop/src/main/services/chat/agentChatService.test.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -909,11 +909,9 @@ async function waitForEvent<T extends AgentChatEventEnvelope>(
909909
}
910910

911911
async function waitForSessionTitle(sessionService: ReturnType<typeof createMockSessionService>, sessionId: string, title: string): Promise<void> {
912-
for (let attempt = 0; attempt < 100; attempt += 1) {
913-
if (sessionService.get(sessionId)?.title === title) return;
914-
await new Promise((resolve) => setTimeout(resolve, 0));
915-
}
916-
throw new Error(`Timed out waiting for session title '${title}'.`);
912+
await vi.waitFor(() => {
913+
expect(sessionService.get(sessionId)?.title).toBe(title);
914+
}, { timeout: 1_000 });
917915
}
918916

919917
// ---------------------------------------------------------------------------
@@ -3363,8 +3361,7 @@ describe("createAgentChatService", () => {
33633361
method: "thread/name/updated",
33643362
params: { threadId: "thread-1", name: "Should Not Win" },
33653363
});
3366-
await new Promise((resolve) => setTimeout(resolve, 20));
3367-
expect(sessionService.get(session.id)?.title).toBe("Manual Title");
3364+
await waitForSessionTitle(sessionService, session.id, "Manual Title");
33683365
});
33693366

33703367
it("lets OpenCode session.updated titles beat ADE AI fallback", async () => {
@@ -3401,6 +3398,9 @@ describe("createAgentChatService", () => {
34013398
});
34023399

34033400
await service.sendMessage({ sessionId: session.id, text: "Use ACP title." }, { awaitDispatch: true });
3401+
await vi.waitFor(() => {
3402+
expect(typeof mockState.droidPooled.bridge.onSessionUpdate).toBe("function");
3403+
}, { timeout: 1_000 });
34043404
mockState.droidPooled.bridge.onSessionUpdate?.({
34053405
sessionId: "droid-acp-session-1",
34063406
update: {
@@ -5801,7 +5801,7 @@ describe("createAgentChatService", () => {
58015801
}
58025802
});
58035803

5804-
it("keeps the requested Codex reasoning effort while applying effective thread policy", async () => {
5804+
it("persists the runtime-effective Codex reasoning effort while applying effective thread policy", async () => {
58055805
mockState.codexResponseOverrides.set("thread/start", () => ({
58065806
thread: { id: "thread-effective-start" },
58075807
approvalPolicy: "onFailure",
@@ -5843,18 +5843,18 @@ describe("createAgentChatService", () => {
58435843
} | undefined;
58445844
expect(turnStartParams?.approvalPolicy).toBe("on-failure");
58455845
expect(turnStartParams?.sandboxPolicy?.type).toBe("workspaceWrite");
5846-
expect(turnStartParams?.effort).toBe("xhigh");
5846+
expect(turnStartParams?.effort).toBe("high");
58475847

58485848
const summary = await service.getSessionSummary(session.id);
58495849
expect(summary?.codexApprovalPolicy).toBe("on-failure");
58505850
expect(summary?.codexSandbox).toBe("workspace-write");
58515851
expect(summary?.permissionMode).toBe("default");
5852-
expect(summary?.reasoningEffort).toBe("xhigh");
5852+
expect(summary?.reasoningEffort).toBe("high");
58535853

58545854
const persisted = readPersistedChatState(session.id);
58555855
expect(persisted.codexApprovalPolicy).toBe("on-failure");
58565856
expect(persisted.codexSandbox).toBe("workspace-write");
5857-
expect(persisted.reasoningEffort).toBe("xhigh");
5857+
expect(persisted.reasoningEffort).toBe("high");
58585858
});
58595859

58605860
it("re-resumes Codex threads when permission mode changes mid-session", async () => {
@@ -6185,7 +6185,7 @@ describe("createAgentChatService", () => {
61856185
expect(sessionService.reopen).toHaveBeenCalledWith(session.id);
61866186
});
61876187

6188-
it("keeps requested Codex reasoning effort while applying effective policy on resume", async () => {
6188+
it("keeps runtime-effective Codex reasoning effort while applying effective policy on resume", async () => {
61896189
mockState.codexResponseOverrides.set("thread/resume", () => ({
61906190
thread: { id: "thread-effective-resume" },
61916191
approvalPolicy: "onFailure",
@@ -6219,13 +6219,13 @@ describe("createAgentChatService", () => {
62196219
expect(resumed.codexApprovalPolicy).toBe("on-failure");
62206220
expect(resumed.codexSandbox).toBe("workspace-write");
62216221
expect(resumed.permissionMode).toBe("default");
6222-
expect(resumed.reasoningEffort).toBe("xhigh");
6222+
expect(resumed.reasoningEffort).toBe("high");
62236223

62246224
const persistedAfter = readPersistedChatState(session.id);
62256225
expect(persistedAfter.threadId).toBe("thread-effective-resume");
62266226
expect(persistedAfter.codexApprovalPolicy).toBe("on-failure");
62276227
expect(persistedAfter.codexSandbox).toBe("workspace-write");
6228-
expect(persistedAfter.reasoningEffort).toBe("xhigh");
6228+
expect(persistedAfter.reasoningEffort).toBe("high");
62296229
});
62306230

62316231
it("throws when resuming an unknown session", async () => {

0 commit comments

Comments
 (0)