Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Only write entries that are worth mentioning to users.
- Web: Show placeholder text in prompt input with hints for slash commands and file mentions
- Web: Fix Ctrl+C not working in uvicorn web server by restoring default SIGINT handler and terminal state after shell mode exits
- Web: Improve session stop handling with proper async cleanup and timeout
- ACP: Add protocol version negotiation framework for client-server compatibility
- ACP: Add session resume method to restore session state (experimental)
- ACP: Add session fork method to clone session state (experimental)

## 1.11.0 (2026-02-10)

Expand Down
3 changes: 2 additions & 1 deletion docs/en/reference/kimi-web.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,13 @@ Session search feature added in version 1.5. Directory auto-creation prompt adde

Web UI provides a unified prompt toolbar above the input box, displaying various information in collapsible tabs:

- **Context usage**: Shows the current context usage percentage. Hover to view detailed token usage breakdown (including input/output tokens, cache read/write, etc.)
- **Activity status**: Shows the current agent state (processing, waiting for approval, etc.)
- **Message queue**: Queue follow-up messages while the AI is processing; queued messages are sent automatically when the current response completes
- **File changes**: Detects Git repository status, showing the number of new, modified, and deleted files (including untracked files). Click to view a detailed list of changes

::: info Changed
Git diff status bar added in version 1.5. Activity status indicator added in version 1.9. Later versions unified it into the prompt toolbar, integrating activity status, message queue, and file changes.
Git diff status bar added in version 1.5. Activity status indicator added in version 1.9. Version 1.10 unified it into the prompt toolbar. Version 1.11 moved the context usage indicator to the prompt toolbar.
:::

### Open-in functionality
Expand Down
3 changes: 3 additions & 0 deletions docs/en/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ This page documents the changes in each Kimi Code CLI release.
- Web: Show placeholder text in prompt input with hints for slash commands and file mentions
- Web: Fix Ctrl+C not working in uvicorn web server by restoring default SIGINT handler and terminal state after shell mode exits
- Web: Improve session stop handling with proper async cleanup and timeout
- ACP: Add protocol version negotiation framework for client-server compatibility
- ACP: Add session resume method to restore session state (experimental)
- ACP: Add session fork method to clone session state (experimental)

## 1.11.0 (2026-02-10)

Expand Down
1 change: 0 additions & 1 deletion docs/zh/configuration/data-locations.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,3 @@ Wire 消息记录文件,以 JSONL 格式存储会话中的 Wire 事件。用
| 清理日志 | 删除 `~/.kimi/logs/` 目录 |
| 清理 MCP 配置 | 删除 `~/.kimi/mcp.json` 或使用 `kimi mcp remove` |
| 清理登录凭据 | 删除 `~/.kimi/credentials/` 目录或使用 `/logout` |

1 change: 0 additions & 1 deletion docs/zh/configuration/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,3 @@ export KIMI_CLI_NO_AUTO_UPDATE="1"
::: tip 提示
如果你通过 Nix 或其他包管理器安装 Kimi Code CLI,通常会自动设置此环境变量,因为更新由包管理器处理。
:::

1 change: 0 additions & 1 deletion docs/zh/configuration/overrides.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,3 @@ max_context_size = 262144
| `KIMI_API_KEY=sk-env kimi` | 配置文件 | 环境变量 | 配置文件 |
| `kimi --model other` | 配置文件 | 配置文件 | CLI 参数 |
| `KIMI_MODEL_NAME=k2 kimi` | 配置文件 | 配置文件 | 环境变量 |

1 change: 0 additions & 1 deletion docs/zh/configuration/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,3 @@ capabilities = ["thinking", "image_in"]
| `moonshot_fetch` | `FetchURL` | 回退到本地抓取 |

使用其他平台时,`FetchURL` 工具仍可使用,但会回退到本地抓取。

1 change: 0 additions & 1 deletion docs/zh/guides/interaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,3 @@ kimi --yolo
::: warning 注意
YOLO 模式会跳过所有确认,请确保你了解可能的风险。建议仅在可控环境中使用。
:::

1 change: 0 additions & 1 deletion docs/zh/guides/use-cases.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,3 @@ Kimi Code CLI 可以执行各种重复性的小任务:
```
把 images 目录下的所有 PNG 图片转换为 JPEG 格式,保存到 output 目录
```

3 changes: 2 additions & 1 deletion docs/zh/reference/kimi-web.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,13 @@ Web UI 提供了便捷的会话管理界面:

Web UI 在输入框上方提供统一的提示工具栏,以可折叠标签页的形式展示多种信息:

- **上下文用量**:显示当前上下文的使用百分比,悬停可查看详细的 Token 用量明细(包括输入/输出 Token、缓存读取/写入等)
- **活动状态**:显示 Agent 当前状态(处理中、等待审批等)
- **消息队列**:在 AI 处理过程中可以排队发送后续消息,待当前回复完成后自动发送
- **文件变更**:检测 Git 仓库状态,显示新增、修改和删除的文件数量(包含未跟踪文件),点击可查看详细的变更列表

::: info 变更
Git diff 状态栏新增于 1.5 版本。1.9 版本添加了活动状态指示器。后续版本将其统一为提示工具栏,整合活动状态、消息队列和文件变更
Git diff 状态栏新增于 1.5 版本。1.9 版本添加了活动状态指示器。1.10 版本将其统一为提示工具栏。1.11 版本将上下文用量指示器移至提示工具栏
:::

### Open-in 功能
Expand Down
3 changes: 3 additions & 0 deletions docs/zh/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
- Web:在提示输入框中显示引导占位文本,提示可使用斜杠命令和 @ 引用文件
- Web:修复在 uvicorn Web 服务器中 Ctrl+C 无法使用的问题,在 Shell 模式退出后恢复默认的 SIGINT 信号处理程序和终端状态
- Web:改进会话停止处理,使用正确的异步清理和超时机制
- ACP:添加协议版本协商框架,用于客户端与服务端之间的兼容性校验
- ACP:添加会话恢复方法,用于恢复会话状态(实验性)
- ACP:添加会话分支(fork)方法,用于克隆会话状态(实验性)

## 1.11.0 (2026-02-10)

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description = "Kimi Code CLI is your next CLI agent."
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"agent-client-protocol==0.7.0",
"agent-client-protocol==0.8.0",
"aiofiles>=24.0,<26.0",
"aiohttp==3.13.3",
"typer==0.21.1",
Expand Down
146 changes: 112 additions & 34 deletions src/kimi_cli/acp/server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import asyncio
import shutil
import sys
from datetime import datetime
from pathlib import Path
Expand All @@ -14,6 +15,7 @@
from kimi_cli.acp.session import ACPSession
from kimi_cli.acp.tools import replace_tools
from kimi_cli.acp.types import ACPContentBlock, MCPServer
from kimi_cli.acp.version import ACPVersionSpec, negotiate_version
from kimi_cli.app import KimiCLI
from kimi_cli.config import LLMModel, load_config, save_config
from kimi_cli.constant import NAME, VERSION
Expand All @@ -29,6 +31,7 @@ def __init__(self) -> None:
self.client_capabilities: acp.schema.ClientCapabilities | None = None
self.conn: acp.Client | None = None
self.sessions: dict[str, tuple[ACPSession, _ModelIDConv]] = {}
self.negotiated_version: ACPVersionSpec | None = None

def on_connect(self, conn: acp.Client) -> None:
logger.info("ACP client connected")
Expand All @@ -41,10 +44,13 @@ async def initialize(
client_info: acp.schema.Implementation | None = None,
**kwargs: Any,
) -> acp.InitializeResponse:
self.negotiated_version = negotiate_version(protocol_version)
logger.info(
"ACP server initialized with protocol version: {version}, "
"ACP server initialized with client protocol version: {version}, "
"negotiated version: {negotiated}, "
"client capabilities: {capabilities}, client info: {info}",
version=protocol_version,
negotiated=self.negotiated_version,
capabilities=client_capabilities,
info=client_info,
)
Expand All @@ -59,15 +65,17 @@ async def initialize(
args = sys.argv[1 : idx + 1]

return acp.InitializeResponse(
protocol_version=protocol_version,
protocol_version=self.negotiated_version.protocol_version,
agent_capabilities=acp.schema.AgentCapabilities(
load_session=True,
prompt_capabilities=acp.schema.PromptCapabilities(
embedded_context=False, image=True, audio=False
),
mcp_capabilities=acp.schema.McpCapabilities(http=True, sse=False),
session_capabilities=acp.schema.SessionCapabilities(
fork=acp.schema.SessionForkCapabilities(),
list=acp.schema.SessionListCapabilities(),
resume=acp.schema.SessionResumeCapabilities(),
),
),
auth_methods=[
Expand Down Expand Up @@ -105,16 +113,14 @@ async def initialize(
agent_info=acp.schema.Implementation(name=NAME, version=VERSION),
)

async def new_session(
self, cwd: str, mcp_servers: list[MCPServer], **kwargs: Any
) -> acp.NewSessionResponse:
logger.info("Creating new session for working directory: {cwd}", cwd=cwd)
async def _init_session(
self, session: Session, mcp_servers: list[MCPServer] | None = None
) -> tuple[ACPSession, _ModelIDConv]:
"""Initialize a KimiCLI instance for a session and register it."""
assert self.conn is not None, "ACP client not connected"
assert self.client_capabilities is not None, "ACP connection not initialized"

session = await Session.create(KaosPath.unsafe_from_local_path(Path(cwd)))

mcp_config = acp_mcp_servers_to_mcp_config(mcp_servers)
mcp_config = acp_mcp_servers_to_mcp_config(mcp_servers or [])
cli_instance = await KimiCLI.create(
session,
mcp_configs=[mcp_config],
Expand All @@ -134,6 +140,18 @@ async def new_session(
cli_instance.soul.runtime,
)

return acp_session, model_id_conv

async def new_session(
self, cwd: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any
) -> acp.NewSessionResponse:
logger.info("Creating new session for working directory: {cwd}", cwd=cwd)
assert self.conn is not None

session = await Session.create(KaosPath.unsafe_from_local_path(Path(cwd)))
acp_session, model_id_conv = await self._init_session(session, mcp_servers)
config = acp_session.cli.soul.runtime.config

available_commands = [
acp.schema.AvailableCommand(name=cmd.name, description=cmd.description)
for cmd in soul_slash_registry.list_commands()
Expand Down Expand Up @@ -165,46 +183,106 @@ async def new_session(
),
)

async def _setup_session(
self,
cwd: str,
session_id: str,
mcp_servers: list[MCPServer] | None = None,
) -> tuple[ACPSession, _ModelIDConv]:
"""Load or resume a session. Shared by load_session and resume_session."""
work_dir = KaosPath.unsafe_from_local_path(Path(cwd))
session = await Session.find(work_dir, session_id)
if session is None:
logger.error(
"Session not found: {id} for working directory: {cwd}", id=session_id, cwd=cwd
)
raise acp.RequestError.invalid_params({"session_id": "Session not found"})

return await self._init_session(session, mcp_servers)

async def load_session(
self, cwd: str, mcp_servers: list[MCPServer], session_id: str, **kwargs: Any
self, cwd: str, session_id: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any
) -> None:
logger.info("Loading session: {id} for working directory: {cwd}", id=session_id, cwd=cwd)
assert self.conn is not None, "ACP client not connected"
assert self.client_capabilities is not None, "ACP connection not initialized"

if session_id in self.sessions:
logger.warning("Session already loaded: {id}", id=session_id)
return

await self._setup_session(cwd, session_id, mcp_servers)
# TODO: replay session history?

async def resume_session(
self, cwd: str, session_id: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any
) -> acp.schema.ResumeSessionResponse:
logger.info("Resuming session: {id} for working directory: {cwd}", id=session_id, cwd=cwd)

if session_id not in self.sessions:
await self._setup_session(cwd, session_id, mcp_servers)

acp_session, model_id_conv = self.sessions[session_id]
config = acp_session.cli.soul.runtime.config
return acp.schema.ResumeSessionResponse(
modes=acp.schema.SessionModeState(
available_modes=[
acp.schema.SessionMode(
id="default",
name="Default",
description="The default mode.",
),
],
current_mode_id="default",
),
models=acp.schema.SessionModelState(
available_models=_expand_llm_models(config.models),
current_model_id=model_id_conv.to_acp_model_id(),
),
)

async def fork_session(
self, cwd: str, session_id: str, mcp_servers: list[MCPServer] | None = None, **kwargs: Any
) -> acp.schema.ForkSessionResponse:
logger.info("Forking session: {id} for working directory: {cwd}", id=session_id, cwd=cwd)

work_dir = KaosPath.unsafe_from_local_path(Path(cwd))
session = await Session.find(work_dir, session_id)
if session is None:
source_session = await Session.find(work_dir, session_id)
if source_session is None:
logger.error(
"Session not found: {id} for working directory: {cwd}", id=session_id, cwd=cwd
"Source session not found: {id} for working directory: {cwd}",
id=session_id,
cwd=cwd,
)
raise acp.RequestError.invalid_params({"session_id": "Session not found"})

mcp_config = acp_mcp_servers_to_mcp_config(mcp_servers)
cli_instance = await KimiCLI.create(
session,
mcp_configs=[mcp_config],
)
config = cli_instance.soul.runtime.config
acp_kaos = ACPKaos(self.conn, session.id, self.client_capabilities)
acp_session = ACPSession(session.id, cli_instance, self.conn, kaos=acp_kaos)
model_id_conv = _ModelIDConv(config.default_model, config.default_thinking)
self.sessions[session.id] = (acp_session, model_id_conv)
new_session = await Session.create(work_dir)

if isinstance(cli_instance.soul.agent.toolset, KimiToolset):
replace_tools(
self.client_capabilities,
self.conn,
session.id,
cli_instance.soul.agent.toolset,
cli_instance.soul.runtime,
)
# Copy context and wire files from source to new session
for filename in ("context.jsonl", "wire.jsonl"):
src_file = source_session.dir / filename
dst_file = new_session.dir / filename
if src_file.exists():
shutil.copy2(src_file, dst_file)

# TODO: replay session history?
acp_session, model_id_conv = await self._init_session(new_session, mcp_servers)
config = acp_session.cli.soul.runtime.config

return acp.schema.ForkSessionResponse(
session_id=new_session.id,
modes=acp.schema.SessionModeState(
available_modes=[
acp.schema.SessionMode(
id="default",
name="Default",
description="The default mode.",
),
],
current_mode_id="default",
),
models=acp.schema.SessionModelState(
available_models=_expand_llm_models(config.models),
current_model_id=model_id_conv.to_acp_model_id(),
),
)
Comment on lines +268 to +285
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Forked session missing AvailableCommandsUpdate notification to ACP client

The fork_session method creates a new session (with a new session_id) but does not send the AvailableCommandsUpdate session update to the ACP client. In contrast, new_session at src/kimi_cli/acp/server.py:154-167 sends this update immediately after session creation so the client knows which slash commands are available.

Root Cause

When new_session creates a session, it builds the available commands list and fires an AvailableCommandsUpdate notification:

available_commands = [
    acp.schema.AvailableCommand(name=cmd.name, description=cmd.description)
    for cmd in soul_slash_registry.list_commands()
]
asyncio.create_task(
    self.conn.session_update(
        session_id=session.id,
        update=acp.schema.AvailableCommandsUpdate(
            session_update="available_commands_update",
            available_commands=available_commands,
        ),
    )
)

The fork_session method at lines 242-285 creates a brand new session with a different session_id but omits this notification entirely. The ACP client (e.g., an IDE integration) will not receive the list of available slash commands for the forked session, so features like command autocomplete won't work.

The project's own src/kimi_cli/acp/AGENTS.md states: "Both send AvailableCommandsUpdate for slash commands on session creation."

Impact: Slash command discovery/autocomplete will be missing in the forked session's client UI.

Suggested change
return acp.schema.ForkSessionResponse(
session_id=new_session.id,
modes=acp.schema.SessionModeState(
available_modes=[
acp.schema.SessionMode(
id="default",
name="Default",
description="The default mode.",
),
],
current_mode_id="default",
),
models=acp.schema.SessionModelState(
available_models=_expand_llm_models(config.models),
current_model_id=model_id_conv.to_acp_model_id(),
),
)
available_commands = [
acp.schema.AvailableCommand(name=cmd.name, description=cmd.description)
for cmd in soul_slash_registry.list_commands()
]
asyncio.create_task(
self.conn.session_update(
session_id=new_session.id,
update=acp.schema.AvailableCommandsUpdate(
session_update="available_commands_update",
available_commands=available_commands,
),
)
)
return acp.schema.ForkSessionResponse(
session_id=new_session.id,
modes=acp.schema.SessionModeState(
available_modes=[
acp.schema.SessionMode(
id="default",
name="Default",
description="The default mode.",
),
],
current_mode_id="default",
),
models=acp.schema.SessionModelState(
available_models=_expand_llm_models(config.models),
current_model_id=model_id_conv.to_acp_model_id(),
),
)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


async def list_sessions(
self, cursor: str | None = None, cwd: str | None = None, **kwargs: Any
Expand Down
45 changes: 45 additions & 0 deletions src/kimi_cli/acp/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from __future__ import annotations

from dataclasses import dataclass


@dataclass(frozen=True)
class ACPVersionSpec:
"""Describes one supported ACP protocol version."""

protocol_version: int # negotiation integer (currently 1)
spec_tag: str # ACP spec tag (e.g. "v0.10.8")
sdk_version: str # corresponding SDK version (e.g. "0.8.0")


CURRENT_VERSION = ACPVersionSpec(
protocol_version=1,
spec_tag="v0.10.8",
sdk_version="0.8.0",
)

SUPPORTED_VERSIONS: dict[int, ACPVersionSpec] = {
1: CURRENT_VERSION,
}

MIN_PROTOCOL_VERSION = 1


def negotiate_version(client_protocol_version: int) -> ACPVersionSpec:
"""Negotiate the protocol version with the client.

Returns the highest server-supported version that does not exceed the
client's requested version. If the client version is lower than
``MIN_PROTOCOL_VERSION`` the server still returns its own current
version so the client can decide whether to disconnect.
"""
if client_protocol_version < MIN_PROTOCOL_VERSION:
return CURRENT_VERSION

# Find the highest supported version <= client version
best: ACPVersionSpec | None = None
for ver, spec in SUPPORTED_VERSIONS.items():
if ver <= client_protocol_version and (best is None or ver > best.protocol_version):
best = spec

return best if best is not None else CURRENT_VERSION
Loading
Loading