Skip to content

Commit 3e1c6bc

Browse files
Merge branch 'main' into refactor/ink-v2
2 parents 91b9366 + d6bfc34 commit 3e1c6bc

5 files changed

Lines changed: 90 additions & 32 deletions

File tree

contributors.svg

Lines changed: 7 additions & 5 deletions
Loading

packages/@ant/computer-use-swift/src/backends/darwin.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -159,25 +159,33 @@ export const apps: AppsAPI = {
159159

160160
async listInstalled() {
161161
try {
162-
const result = await osascript(`
163-
tell application "System Events"
164-
set appList to ""
165-
repeat with appFile in (every file of folder "Applications" of startup disk whose name ends with ".app")
166-
set appPath to POSIX path of (appFile as alias)
167-
set appName to name of appFile
168-
set appList to appList & appPath & "|" & appName & "\\n"
169-
end repeat
170-
return appList
171-
end tell
172-
`)
173-
return result.split('\n').filter(Boolean).map(line => {
174-
const [path, name] = line.split('|', 2)
175-
const displayName = (name ?? '').replace(/\.app$/, '')
176-
return {
177-
bundleId: `com.app.${displayName.toLowerCase().replace(/\s+/g, '-')}`,
178-
displayName,
179-
path: path ?? '',
162+
// Use mdls to enumerate apps and get real bundle identifiers.
163+
// The previous AppleScript approach generated fake bundle IDs
164+
// (com.app.display-name) which prevented request_access from matching
165+
// apps by their real bundle ID (e.g. com.google.Chrome).
166+
const dirs = ['/Applications', '~/Applications', '/System/Applications']
167+
const allApps: InstalledApp[] = []
168+
for (const dir of dirs) {
169+
const expanded = dir.startsWith('~') ? join(process.env.HOME ?? '~', dir.slice(1)) : dir
170+
const proc = Bun.spawn(
171+
['bash', '-c', `for f in "${expanded}"/*.app; do [ -d "$f" ] || continue; bid=$(mdls -name kMDItemCFBundleIdentifier "$f" 2>/dev/null | sed 's/.*= "//;s/"//'); name=$(basename "$f" .app); echo "$f|$name|$bid"; done`],
172+
{ stdout: 'pipe', stderr: 'pipe' },
173+
)
174+
const text = await new Response(proc.stdout).text()
175+
await proc.exited
176+
for (const line of text.split('\n').filter(Boolean)) {
177+
const [path, displayName, bundleId] = line.split('|', 3)
178+
if (path && displayName && bundleId && bundleId !== '(null)') {
179+
allApps.push({ bundleId, displayName, path })
180+
}
180181
}
182+
}
183+
// Deduplicate by bundleId (prefer /Applications over ~/Applications)
184+
const seen = new Set<string>()
185+
return allApps.filter(app => {
186+
if (seen.has(app.bundleId)) return false
187+
seen.add(app.bundleId)
188+
return true
181189
})
182190
} catch {
183191
return []

src/services/api/openai/convertMessages.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@ function convertInternalUserMessage(
9292
}
9393
}
9494

95+
// CRITICAL: tool messages must come BEFORE any user message in the result.
96+
// OpenAI API requires that a tool message immediately follows the assistant
97+
// message with tool_calls. If we emit a user message first, the API will
98+
// reject the request with "insufficient tool messages following tool_calls".
99+
// See: https://github.com/anthropics/claude-code/issues/xxx
100+
for (const tr of toolResults) {
101+
result.push(convertToolResult(tr))
102+
}
103+
95104
// 如果有图片,构建多模态 content 数组
96105
if (imageParts.length > 0) {
97106
const multiContent: Array<{ type: 'text'; text: string } | { type: 'image_url'; image_url: { url: string } }> = []
@@ -109,10 +118,6 @@ function convertInternalUserMessage(
109118
content: textParts.join('\n'),
110119
} satisfies ChatCompletionUserMessageParam)
111120
}
112-
113-
for (const tr of toolResults) {
114-
result.push(convertToolResult(tr))
115-
}
116121
}
117122

118123
return result

src/utils/computerUse/escHotkey.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function registerEscHotkey(onEscape: () => void): boolean {
2626
if (process.platform !== 'darwin') return false
2727
if (registered) return true
2828
const cu = requireComputerUseSwift()
29-
if (!(cu as any).hotkey.registerEscape(onEscape)) {
29+
if (!(cu as any).hotkey?.registerEscape(onEscape)) {
3030
// CGEvent.tapCreate failed — typically missing Accessibility permission.
3131
// CU still works, just without ESC abort. Mirrors Cowork's escAbort.ts:81.
3232
logForDebugging('[cu-esc] registerEscape returned false', { level: 'warn' })
@@ -41,7 +41,7 @@ export function registerEscHotkey(onEscape: () => void): boolean {
4141
export function unregisterEscHotkey(): void {
4242
if (!registered) return
4343
try {
44-
(requireComputerUseSwift() as any).hotkey.unregister()
44+
(requireComputerUseSwift() as any).hotkey?.unregister()
4545
} finally {
4646
releasePump()
4747
registered = false
@@ -51,5 +51,5 @@ export function unregisterEscHotkey(): void {
5151

5252
export function notifyExpectedEscape(): void {
5353
if (!registered) return
54-
(requireComputerUseSwift() as any).hotkey.notifyExpectedEscape()
54+
(requireComputerUseSwift() as any).hotkey?.notifyExpectedEscape()
5555
}

src/utils/computerUse/hostAdapter.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,38 @@ class DebugLogger implements Logger {
2727
}
2828
}
2929

30+
// ---------------------------------------------------------------------------
31+
// JXA-based TCC permission probes (fallback when native .node module absent)
32+
// ---------------------------------------------------------------------------
33+
34+
/** Probe accessibility by asking System Events for a process list. */
35+
function checkAccessibilityJXA(): boolean {
36+
try {
37+
const result = Bun.spawnSync({
38+
cmd: ['osascript', '-e', 'tell application "System Events" to get name of every process whose background only is false'],
39+
stdout: 'pipe',
40+
stderr: 'pipe',
41+
})
42+
return result.exitCode === 0
43+
} catch {
44+
return false
45+
}
46+
}
47+
48+
/** Probe screen recording by attempting a 1x1 screencapture. */
49+
function checkScreenRecordingJXA(): boolean {
50+
try {
51+
const result = Bun.spawnSync({
52+
cmd: ['screencapture', '-x', '-R', '0,0,1,1', '/dev/null'],
53+
stdout: 'pipe',
54+
stderr: 'pipe',
55+
})
56+
return result.exitCode === 0
57+
} catch {
58+
return false
59+
}
60+
}
61+
3062
let cached: ComputerUseHostAdapter | undefined
3163

3264
/**
@@ -47,8 +79,19 @@ export function getComputerUseHostAdapter(): ComputerUseHostAdapter {
4779
ensureOsPermissions: async () => {
4880
if (process.platform !== 'darwin') return { granted: true }
4981
const cu = requireComputerUseSwift()
50-
const accessibility = (cu as any).tcc.checkAccessibility()
51-
const screenRecording = (cu as any).tcc.checkScreenRecording()
82+
const tcc = (cu as any).tcc
83+
// Native Swift .node module provides tcc.checkAccessibility/checkScreenRecording.
84+
// When absent (decompiled/reverse-engineered build), fall back to JXA probes.
85+
if (tcc) {
86+
const accessibility = tcc.checkAccessibility()
87+
const screenRecording = tcc.checkScreenRecording()
88+
return accessibility && screenRecording
89+
? { granted: true }
90+
: { granted: false, accessibility, screenRecording }
91+
}
92+
// JXA fallback: try to query System Events (accessibility) and screencapture (screen recording).
93+
const accessibility = checkAccessibilityJXA()
94+
const screenRecording = checkScreenRecordingJXA()
5295
return accessibility && screenRecording
5396
? { granted: true }
5497
: { granted: false, accessibility, screenRecording }

0 commit comments

Comments
 (0)