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',
deepagents:
'\n',
+ "deepseek-tui":
+ '\n',
dimcode:
'\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(
{