diff --git a/packages/app/src/assets/acp-provider-icons.ts b/packages/app/src/assets/acp-provider-icons.ts index 527a78e45..2abd45b48 100644 --- a/packages/app/src/assets/acp-provider-icons.ts +++ b/packages/app/src/assets/acp-provider-icons.ts @@ -27,6 +27,8 @@ export const ACP_PROVIDER_ICON_SVGS = { '\n \n\n', deepagents: '\n \n \n \n \n\n', + "deepseek-tui": + '\n \n 深\n \n\n', dimcode: '\n\n\n', dirac: diff --git a/packages/app/src/assets/acp-provider-icons/deepseek-tui.svg b/packages/app/src/assets/acp-provider-icons/deepseek-tui.svg new file mode 100644 index 000000000..afad0032b --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/deepseek-tui.svg @@ -0,0 +1,5 @@ + + + 深 + + diff --git a/packages/app/src/data/acp-provider-catalog.ts b/packages/app/src/data/acp-provider-catalog.ts index 85895109c..670500994 100644 --- a/packages/app/src/data/acp-provider-catalog.ts +++ b/packages/app/src/data/acp-provider-catalog.ts @@ -135,6 +135,15 @@ const CATALOG_DATA = [ installLink: "https://docs.langchain.com/oss/javascript/deepagents/overview", command: ["npx", "-y", "deepagents-acp@0.1.7"], }, + { + id: "deepseek-tui", + title: "DeepSeek TUI", + description: "Terminal coding agent for DeepSeek V4 with ACP support through its stdio server.", + version: "0.8.39", + iconId: "deepseek-tui", + installLink: "https://github.com/Hmbown/DeepSeek-TUI", + command: ["deepseek", "serve", "--acp"], + }, { id: "dimcode", title: "DimCode", diff --git a/packages/app/src/hooks/use-acp-provider-catalog.test.ts b/packages/app/src/hooks/use-acp-provider-catalog.test.ts index 13ff91a1a..4ecbf0368 100644 --- a/packages/app/src/hooks/use-acp-provider-catalog.test.ts +++ b/packages/app/src/hooks/use-acp-provider-catalog.test.ts @@ -37,6 +37,7 @@ describe("ACP provider catalog", () => { it("uses PATH commands for entries that were binary distributions upstream", () => { expect(findProvider("amp-acp").command).toEqual(["amp-acp"]); expect(findProvider("cursor").command).toEqual(["cursor-agent", "acp"]); + expect(findProvider("deepseek-tui").command).toEqual(["deepseek", "serve", "--acp"]); expect(findProvider("goose").command).toEqual(["goose", "acp"]); expect(findProvider("junie").command).toEqual(["junie", "--acp", "true"]); expect(findProvider("kiro").command).toEqual(["kiro-cli", "acp"]); diff --git a/packages/server/src/server/agent/providers/acp-agent.test.ts b/packages/server/src/server/agent/providers/acp-agent.test.ts index 1f3e9fe30..e80b33f83 100644 --- a/packages/server/src/server/agent/providers/acp-agent.test.ts +++ b/packages/server/src/server/agent/providers/acp-agent.test.ts @@ -362,6 +362,33 @@ describe("createLoggedNdJsonStream", () => { consoleError.mockRestore(); }); + test("normalizes stringified numeric ACP response ids", async () => { + const input = new TransformStream(); + const output = new TransformStream(); + const logger = { + warn: vi.fn(), + }; + + const stream = createLoggedNdJsonStream(output.writable, input.readable, { + logger: asInternals>(logger), + provider: "deepseek-tui", + }); + const reader = stream.readable.getReader(); + const writer = input.writable.getWriter(); + + await writer.write( + new TextEncoder().encode('{"jsonrpc":"2.0","id":"0","result":{"ok":true}}\n'), + ); + + const parsed = await reader.read(); + + expect(parsed.value).toEqual({ jsonrpc: "2.0", id: 0, result: { ok: true } }); + expect(logger.warn).not.toHaveBeenCalled(); + + await writer.close(); + reader.releaseLock(); + }); + test("does not log terminal control sequences from malformed ACP stdout", async () => { const input = new TransformStream(); const output = new TransformStream(); diff --git a/packages/server/src/server/agent/providers/acp-agent.ts b/packages/server/src/server/agent/providers/acp-agent.ts index dbf2e3d02..64bd89397 100644 --- a/packages/server/src/server/agent/providers/acp-agent.ts +++ b/packages/server/src/server/agent/providers/acp-agent.ts @@ -180,6 +180,26 @@ function summarizeMalformedACPStdoutError(error: unknown): { type: string; messa }; } +function normalizeACPIncomingMessage(message: AnyMessage): AnyMessage { + if ( + "id" in message && + !("method" in message) && + typeof message.id === "string" && + /^\d+$/.test(message.id) + ) { + const numericId = Number(message.id); + if (Number.isSafeInteger(numericId)) { + return { + ...message, + // COMPAT(deepseek-tui-acp-id): added v0.1.78, remove after 2026-11-19 + // once the ACP SDK accepts stringified numeric response IDs. + id: numericId, + } as AnyMessage; + } + } + return message; +} + export function createLoggedNdJsonStream( output: NodeWritableStream, input: NodeReadableStream, @@ -214,7 +234,7 @@ export function createLoggedNdJsonStream( try { const message: AnyMessage = JSON.parse(trimmedLine); - controller.enqueue(message); + controller.enqueue(normalizeACPIncomingMessage(message)); } catch (error) { options.logger.warn( {