Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f47852e8e |
@@ -1,7 +1,6 @@
|
||||
import { randomUUID } from "node:crypto"
|
||||
import { EventEmitter } from "node:events"
|
||||
import { existsSync } from "node:fs"
|
||||
import * as http from "node:http"
|
||||
import { createServer } from "node:net"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
@@ -78,7 +77,6 @@ setupApp()
|
||||
|
||||
function setupApp() {
|
||||
ensureLoopbackNoProxy()
|
||||
useEnvProxy()
|
||||
app.commandLine.appendSwitch("proxy-bypass-list", "<-loopback>")
|
||||
if (!app.isPackaged) app.commandLine.appendSwitch("remote-debugging-port", "9222")
|
||||
|
||||
@@ -135,15 +133,6 @@ function useSystemCertificates() {
|
||||
}
|
||||
}
|
||||
|
||||
function useEnvProxy() {
|
||||
try {
|
||||
// Electron 41.2 runs Node 24.14.1; latest @types/node@24 is 24.12.2.
|
||||
;(http as any).setGlobalProxyFromEnv()
|
||||
} catch (error) {
|
||||
logger.warn("failed to load proxy environment", error)
|
||||
}
|
||||
}
|
||||
|
||||
function emitDeepLinks(urls: string[]) {
|
||||
if (urls.length === 0) return
|
||||
pendingDeepLinks.push(...urls)
|
||||
@@ -200,10 +189,7 @@ async function initialize() {
|
||||
}
|
||||
|
||||
logger.log("spawning sidecar", { url })
|
||||
const { listener, health } = await spawnLocalServer(hostname, port, password, () => {
|
||||
ensureLoopbackNoProxy()
|
||||
useEnvProxy()
|
||||
})
|
||||
const { listener, health } = await spawnLocalServer(hostname, port, password)
|
||||
server = listener
|
||||
serverReady.resolve({
|
||||
url,
|
||||
|
||||
@@ -70,14 +70,10 @@ export function registerIpcHandlers(deps: Deps) {
|
||||
ipcMain.handle("install-update", () => deps.installUpdate())
|
||||
ipcMain.handle("set-background-color", (_event: IpcMainInvokeEvent, color: string) => deps.setBackgroundColor(color))
|
||||
ipcMain.handle("store-get", (_event: IpcMainInvokeEvent, name: string, key: string) => {
|
||||
try {
|
||||
const store = getStore(name)
|
||||
const value = store.get(key)
|
||||
if (value === undefined || value === null) return null
|
||||
return typeof value === "string" ? value : JSON.stringify(value)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
const store = getStore(name)
|
||||
const value = store.get(key)
|
||||
if (value === undefined || value === null) return null
|
||||
return typeof value === "string" ? value : JSON.stringify(value)
|
||||
})
|
||||
ipcMain.handle("store-set", (_event: IpcMainInvokeEvent, name: string, key: string, value: string) => {
|
||||
getStore(name).set(key, value)
|
||||
|
||||
@@ -30,9 +30,8 @@ export function setWslConfig(config: WslConfig) {
|
||||
getStore().set(WSL_ENABLED_KEY, config.enabled)
|
||||
}
|
||||
|
||||
export async function spawnLocalServer(hostname: string, port: number, password: string, configureEnv?: () => void) {
|
||||
export async function spawnLocalServer(hostname: string, port: number, password: string) {
|
||||
prepareServerEnv(password)
|
||||
configureEnv?.()
|
||||
const { Log, Server } = await import("virtual:opencode-server")
|
||||
await Log.init({ level: "WARN" })
|
||||
const listener = await Server.listen({
|
||||
|
||||
@@ -122,7 +122,7 @@ function normalizeMessages(
|
||||
})
|
||||
}
|
||||
if (["@ai-sdk/anthropic", "@ai-sdk/google-vertex/anthropic"].includes(model.api.npm)) {
|
||||
// Anthropic rejects assistant turns where client tool_use blocks are followed by non-tool
|
||||
// Anthropic rejects assistant turns where tool_use blocks are followed by non-tool
|
||||
// content, e.g. [tool_use, tool_use, text], with:
|
||||
// `tool_use` ids were found without `tool_result` blocks immediately after...
|
||||
//
|
||||
@@ -130,26 +130,19 @@ function normalizeMessages(
|
||||
// assistant messages are later merged by the provider/SDK, so preserving the
|
||||
// original [tool_use...] then [text] order still produces the invalid payload.
|
||||
//
|
||||
// Provider-executed tools are different: the AI SDK intentionally represents
|
||||
// their tool-call and tool-result together in assistant content.
|
||||
// The root cause appears to be somewhere upstream where the stream is originally
|
||||
// processed. We were unable to locate an exact narrower reproduction elsewhere,
|
||||
// so we keep this transform in place for the time being.
|
||||
msgs = msgs.flatMap((msg) => {
|
||||
if (msg.role !== "assistant" || !Array.isArray(msg.content)) return [msg]
|
||||
|
||||
const parts = msg.content
|
||||
const providerExecuted = new Set(
|
||||
parts.flatMap((part) => (part.type === "tool-call" && part.providerExecuted === true ? [part.toolCallId] : [])),
|
||||
)
|
||||
const isClientToolPart = (part: (typeof parts)[number]) => {
|
||||
if (part.type === "tool-call") return part.providerExecuted !== true
|
||||
if (part.type === "tool-result") return !providerExecuted.has(part.toolCallId)
|
||||
return false
|
||||
}
|
||||
const first = parts.findIndex((part) => part.type === "tool-call" && part.providerExecuted !== true)
|
||||
const first = parts.findIndex((part) => part.type === "tool-call")
|
||||
if (first === -1) return [msg]
|
||||
if (!parts.slice(first).some((part) => !isClientToolPart(part))) return [msg]
|
||||
if (!parts.slice(first).some((part) => part.type !== "tool-call")) return [msg]
|
||||
return [
|
||||
{ ...msg, content: parts.filter((part) => !isClientToolPart(part)) },
|
||||
{ ...msg, content: parts.filter(isClientToolPart) },
|
||||
{ ...msg, content: parts.filter((part) => part.type !== "tool-call") },
|
||||
{ ...msg, content: parts.filter((part) => part.type === "tool-call") },
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1104,32 +1104,6 @@ export function filterCompacted(msgs: Iterable<WithParts>) {
|
||||
completed.add(msg.info.parentID)
|
||||
}
|
||||
result.reverse()
|
||||
const compactionIndex = result.findLastIndex(
|
||||
(msg) =>
|
||||
msg.info.role === "user" &&
|
||||
msg.parts.some((item): item is CompactionPart => item.type === "compaction" && item.tail_start_id !== undefined),
|
||||
)
|
||||
const compaction = result[compactionIndex]
|
||||
const part = compaction?.parts.find(
|
||||
(item): item is CompactionPart => item.type === "compaction" && item.tail_start_id !== undefined,
|
||||
)
|
||||
const summaryIndex = compaction
|
||||
? result.findIndex(
|
||||
(msg, index) =>
|
||||
index > compactionIndex &&
|
||||
msg.info.role === "assistant" &&
|
||||
msg.info.summary &&
|
||||
msg.info.parentID === compaction.info.id,
|
||||
)
|
||||
: -1
|
||||
const tailIndex = part?.tail_start_id ? result.findIndex((msg) => msg.info.id === part.tail_start_id) : -1
|
||||
if (tailIndex >= 0 && tailIndex < compactionIndex && summaryIndex > compactionIndex) {
|
||||
return [
|
||||
...result.slice(compactionIndex, summaryIndex + 1),
|
||||
...result.slice(tailIndex, compactionIndex),
|
||||
...result.slice(summaryIndex + 1),
|
||||
]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -1498,117 +1498,6 @@ describe("ProviderTransform.message - anthropic empty content filtering", () =>
|
||||
])
|
||||
})
|
||||
|
||||
test("keeps tool-call and tool-result paired when splitting anthropic messages", () => {
|
||||
const msgs = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } },
|
||||
{ type: "tool-result", toolCallId: "toolu_1", toolName: "read", output: { type: "text", value: "ok" } },
|
||||
{ type: "text", text: "I checked your home directory." },
|
||||
],
|
||||
},
|
||||
] as any[]
|
||||
|
||||
const result = ProviderTransform.message(msgs, anthropicModel, {}) as any[]
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0]).toMatchObject({
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "I checked your home directory." }],
|
||||
})
|
||||
expect(result[1]).toMatchObject({
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } },
|
||||
{ type: "tool-result", toolCallId: "toolu_1", toolName: "read", output: { type: "text", value: "ok" } },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
test("leaves provider-executed anthropic tool results with trailing text unchanged", () => {
|
||||
const msgs = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "srvtoolu_1",
|
||||
toolName: "code_execution",
|
||||
input: { code: "print('ok')", type: "programmatic-tool-call" },
|
||||
providerExecuted: true,
|
||||
},
|
||||
{
|
||||
type: "tool-result",
|
||||
toolCallId: "srvtoolu_1",
|
||||
toolName: "code_execution",
|
||||
output: {
|
||||
type: "json",
|
||||
value: { type: "code_execution_result", stdout: "ok", stderr: "", return_code: 0 },
|
||||
},
|
||||
},
|
||||
{ type: "text", text: "The code ran successfully." },
|
||||
],
|
||||
},
|
||||
] as any[]
|
||||
|
||||
const result = ProviderTransform.message(msgs, anthropicModel, {}) as any[]
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].content).toMatchObject(msgs[0].content)
|
||||
})
|
||||
|
||||
test("keeps provider-executed pairs together when splitting mixed anthropic tool parts", () => {
|
||||
const msgs = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } },
|
||||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "srvtoolu_1",
|
||||
toolName: "code_execution",
|
||||
input: { code: "print('ok')", type: "programmatic-tool-call" },
|
||||
providerExecuted: true,
|
||||
},
|
||||
{
|
||||
type: "tool-result",
|
||||
toolCallId: "srvtoolu_1",
|
||||
toolName: "code_execution",
|
||||
output: {
|
||||
type: "json",
|
||||
value: { type: "code_execution_result", stdout: "ok", stderr: "", return_code: 0 },
|
||||
},
|
||||
},
|
||||
{ type: "text", text: "The server-side tool ran successfully." },
|
||||
],
|
||||
},
|
||||
] as any[]
|
||||
|
||||
const result = ProviderTransform.message(msgs, anthropicModel, {}) as any[]
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].content).toMatchObject([
|
||||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "srvtoolu_1",
|
||||
toolName: "code_execution",
|
||||
input: { code: "print('ok')", type: "programmatic-tool-call" },
|
||||
providerExecuted: true,
|
||||
},
|
||||
{
|
||||
type: "tool-result",
|
||||
toolCallId: "srvtoolu_1",
|
||||
toolName: "code_execution",
|
||||
output: { type: "json", value: { type: "code_execution_result", stdout: "ok", stderr: "", return_code: 0 } },
|
||||
},
|
||||
{ type: "text", text: "The server-side tool ran successfully." },
|
||||
])
|
||||
expect(result[1].content).toMatchObject([
|
||||
{ type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } },
|
||||
])
|
||||
})
|
||||
|
||||
test("splits vertex anthropic assistant messages when text trails tool calls", () => {
|
||||
const model = {
|
||||
...anthropicModel,
|
||||
|
||||
@@ -1218,9 +1218,7 @@ describe("session.compaction.process", () => {
|
||||
expect(captured).not.toContain("keep tail")
|
||||
|
||||
const filtered = MessageV2.filterCompacted(MessageV2.stream(session.id))
|
||||
expect(filtered.map((msg) => msg.info.id).slice(0, 3)).toEqual([parent!, expect.any(String), keep.id])
|
||||
expect(filtered[1]?.info.role).toBe("assistant")
|
||||
expect(filtered[1]?.info.role === "assistant" ? filtered[1].info.summary : false).toBe(true)
|
||||
expect(filtered[0]?.info.id).toBe(keep.id)
|
||||
expect(filtered.map((msg) => msg.info.id)).not.toContain(large.id)
|
||||
} finally {
|
||||
await rt.dispose()
|
||||
|
||||
@@ -834,7 +834,7 @@ describe("MessageV2.filterCompacted", () => {
|
||||
|
||||
const result = MessageV2.filterCompacted(MessageV2.stream(session.id))
|
||||
|
||||
expect(result.map((item) => item.info.id)).toEqual([c1, s1, u2, a2, u3, a3])
|
||||
expect(result.map((item) => item.info.id)).toEqual([u2, a2, c1, s1, u3, a3])
|
||||
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
@@ -889,7 +889,7 @@ describe("MessageV2.filterCompacted", () => {
|
||||
})
|
||||
|
||||
const parentFiltered = MessageV2.filterCompacted(MessageV2.stream(session.id))
|
||||
expect(parentFiltered.map((item) => item.info.id)).toEqual([c1, s1, u2, a2, u3, a3])
|
||||
expect(parentFiltered.map((item) => item.info.id)).toEqual([u2, a2, c1, s1, u3, a3])
|
||||
|
||||
const forked = await svc.fork({ sessionID: session.id })
|
||||
const childFiltered = MessageV2.filterCompacted(MessageV2.stream(forked.id))
|
||||
@@ -964,7 +964,7 @@ describe("MessageV2.filterCompacted", () => {
|
||||
|
||||
const result = MessageV2.filterCompacted(MessageV2.stream(session.id))
|
||||
|
||||
expect(result.map((item) => item.info.id)).toEqual([c1, s1, a3, u3, a4])
|
||||
expect(result.map((item) => item.info.id)).toEqual([a3, c1, s1, u3, a4])
|
||||
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
@@ -1041,7 +1041,7 @@ describe("MessageV2.filterCompacted", () => {
|
||||
|
||||
const result = MessageV2.filterCompacted(MessageV2.stream(session.id))
|
||||
|
||||
expect(result.map((item) => item.info.id)).toEqual([c2, s2, u3, a3, u4, a4])
|
||||
expect(result.map((item) => item.info.id)).toEqual([u3, a3, c2, s2, u4, a4])
|
||||
|
||||
await svc.remove(session.id)
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user