Skip to content

[WIP] support acp#4707

Open
lulusir wants to merge 13 commits intomainfrom
feat/acp
Open

[WIP] support acp#4707
lulusir wants to merge 13 commits intomainfrom
feat/acp

Conversation

@lulusir
Copy link
Contributor

@lulusir lulusir commented Mar 16, 2026

Types

  • 🎉 New Features
  • 🐛 Bug Fixes
  • 📚 Documentation Changes
  • 💄 Code Style Changes
  • 💄 Style Changes
  • 🪚 Refactors
  • 🚀 Performance Improvements
  • 🏗️ Build System
  • ⏱ Tests
  • 🧹 Chores
  • Other Changes

Background or solution

Changelog

Summary by CodeRabbit

发布说明

  • 新功能

    • 添加 ACP(Agent Client Protocol)代理集成与会话管理支持
    • 引入权限对话框系统,用户可对敏感操作进行授权
    • 支持多种会话源(本地与ACP),实现灵活的会话持久化
    • 增加模式切换功能,支持不同的AI代理模式
    • 优化移动设备布局显示
  • 改进

    • 增强会话提供者架构,支持可插拔的会话数据源
    • 改进权限管理流程,提供更好的用户交互体验
  • 依赖

    • 添加 @agentclientprotocol/sdk 依赖

@lulusir
Copy link
Contributor Author

lulusir commented Mar 16, 2026

/next

@lulusir lulusir changed the title Feat/acp support acp [WIP] support acp Mar 16, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 16, 2026

概览

本PR引入了完整的Agent Client Protocol (ACP) 支持,包括权限对话框系统、会话管理重构、CLI代理进程管理、以及ACP特定的后端服务。这是一个大规模的特性实现,涉及浏览器端和Node端的多个层级。

变更

队列 / 文件 摘要
ACP权限系统
packages/ai-native/src/browser/acp/permission-bridge.service.ts, packages/ai-native/src/browser/acp/permission.handler.ts, packages/ai-native/src/browser/acp/acp-permission-rpc.service.ts, packages/ai-native/src/node/acp/acp-permission-caller.service.ts
实现权限对话框的完整生命周期管理,包括请求显示、用户决策处理、超时管理和RPC通信。支持权限规则持久化和自动决策。
权限UI组件
packages/ai-native/src/browser/acp/permission-dialog.view.tsx, packages/ai-native/src/browser/acp/permission-dialog-container.tsx, packages/ai-native/src/browser/components/permission-dialog-widget.tsx, packages/ai-native/src/browser/acp/permission-dialog*.module.less
新增权限对话框React组件,支持键盘导航、倒计时、选项渲染和条件式关闭。包含对应的样式模块和管理器。
ACP权限导出
packages/ai-native/src/browser/acp/index.ts
统一导出所有ACP权限相关的公共API。
会话提供者系统
packages/ai-native/src/browser/chat/session-provider.ts, packages/ai-native/src/browser/chat/session-provider-registry.ts, packages/ai-native/src/browser/chat/local-storage-provider.ts, packages/ai-native/src/browser/chat/acp-session-provider.ts
引入可插拔的会话管理框架,支持本地和ACP两种源,包括会话注册表和提供者路由机制。
Chat管理器重构
packages/ai-native/src/browser/chat/chat-manager.service.ts
将会话持久化从直接存储改为基于提供者的方案;更新init、startSession、getSession、createRequest、sendRequest为异步;增加主提供者选择和会话缓存管理。
Chat模型扩展
packages/ai-native/src/browser/chat/chat-model.ts
添加title属性、getRequests方法;改进JSON解析的错误处理。
默认Chat代理
packages/ai-native/src/browser/chat/default-chat-agent.ts
引入DefaultChatAgent实现,提供系统提示、请求选项构建、斜杠命令和欢迎消息支持。
ACP Chat代理
packages/ai-native/src/browser/chat/acp-chat-agent.ts
实现IChatAgent接口的ACP特定代理,支持工作空间上下文、MCP禁用工具和流式响应。
Chat代理工厂
packages/ai-native/src/browser/chat/chat-proxy.service.ts
将代理注册从内联实现改为工厂模式,依赖注入DefaultChatAgent。
Chat UI更新
packages/ai-native/src/browser/chat/chat.view.tsx, packages/ai-native/src/browser/chat/chat.internal.service.ts, packages/ai-native/src/browser/components/ChatMentionInput.tsx, packages/ai-native/src/browser/components/mention-input/mention-input.tsx, packages/ai-native/src/browser/components/mention-input/types.ts
添加会话加载状态反馈、模式选择、空安全保护和权限对话框集成。
Chat相关文档
packages/ai-native/src/browser/chat/chat-agent.service.ts, packages/ai-native/src/browser/chat/apply.service.ts, packages/ai-native/src/browser/chat/chat-multi-diff-source.ts, packages/ai-native/src/browser/chat/chat.api.service.ts, packages/ai-native/src/browser/chat/chat.feature.registry.ts, packages/ai-native/src/browser/chat/chat.render.registry.ts
添加文件头部文档注释,增强代码可维护性。
Chat资源编辑
packages/ai-native/src/browser/chat/chat-edit-resource.ts
引入ChatEditSchemeDocumentProvider支持Chat编辑源码的读取。
布局更新
packages/ai-native/src/browser/layout/ai-layout.tsx
添加移动设备检测和响应式布局调整。
浏览器端DI配置
packages/ai-native/src/browser/index.ts
注册所有新的ACP服务、会话提供者、权限桥接器和对话框组件;更新默认Chat代理token绑定;修改Chat代理提示提供者工厂逻辑。
CLI代理进程管理
packages/ai-native/src/node/acp/cli-agent-process-manager.ts
实现单例模式的CLI代理子进程生命周期管理,支持启动、停止、强制杀死和状态查询。
ACP CLI客户端
packages/ai-native/src/node/acp/acp-cli-client.service.ts
实现JSON-RPC 2.0 over NDJSON的ACP CLI客户端,支持会话管理、文件操作、权限请求和终端控制。
ACP代理服务
packages/ai-native/src/node/acp/acp-agent.service.ts
实现单个ACP代理的会话和消息管理,支持流式更新、权限处理和会话模式切换。
ACP后端服务
packages/ai-native/src/node/acp/acp-cli-back.service.ts
实现IAIBackService接口,将ACP代理与Chat系统集成,处理流式响应和会话转换。
ACP请求处理器
packages/ai-native/src/node/acp/handlers/agent-request.handler.ts
统一处理所有ACP代理请求,包括文件操作、权限确认和终端控制。
文件系统处理器
packages/ai-native/src/node/acp/handlers/file-system.handler.ts
实现沙箱化的工作空间文件操作(读、写、元数据、列表、创建目录),支持权限回调。
终端处理器
packages/ai-native/src/node/acp/handlers/terminal.handler.ts
实现终端生命周期管理,支持创建、输出流、等待退出、杀死和资源释放,包含会话级别的追踪。
错误常量
packages/ai-native/src/node/acp/handlers/constants.ts
定义JSON-RPC和ACP特定的错误代码。
ACP索引导出
packages/ai-native/src/node/acp/index.ts
统一导出所有Node端ACP服务和处理器。
Node端DI配置
packages/ai-native/src/node/index.ts
从BaseAIBackService切换到AcpCliBackService;注册所有新的ACP服务、客户端和处理器。
类型定义
packages/ai-native/src/common/acp-types.ts, packages/ai-native/src/common/agent-types.ts, packages/ai-native/src/common/index.ts
定义ACP权限、CLI客户端、代理配置和会话模式的类型和接口。
提示提供者
packages/ai-native/src/common/prompts/empty-prompt-provider.ts
实现ACPChatAgentPromptProvider,直接返回用户消息(无处理)。
核心类型扩展
packages/core-common/src/types/ai-native/index.ts
扩展IAINativeCapabilities、IAIBackServiceOption和IAIBackService,支持代理模式和会话管理。
配置更新
packages/core-browser/src/ai-native/ai-config.service.ts, packages/core-common/src/storage.ts
添加supportsAgentMode能力和AI_NATIVE存储命名空间。
包依赖
packages/ai-native/package.json
添加@agentclientprotocol/sdk ^0.16.1依赖。
测试套件
packages/ai-native/__test__/browser/chat/chat-manager.service.test.ts, packages/ai-native/__test__/node/acp/cli-agent-process-manager.test.ts
为ChatManagerService和CliAgentProcessManager添加全面的单元测试,覆盖初始化、会话生命周期、请求处理和进程管理。
启动配置
packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts, packages/startup/entry/web/server.ts
移除或禁用特定的服务绑定,迁移到ACP的DI配置。
日志配置
packages/core-common/src/log.ts
禁用基于环境变量的调试日志自动启用。
时间显示
packages/ai-native/src/browser/components/ChatHistory.tsx
隐藏历史记录组的时间标签。

序列图

sequenceDiagram
    participant User as 用户
    participant UI as 浏览器UI
    participant PermBridge as PermissionBridge<br/>(权限桥接)
    participant RPC as RPC Service<br/>(权限RPC)
    participant NodeACP as Node ACP<br/>(权限调用者)
    participant Agent as ACP代理<br/>(CLI进程)

    User->>UI: 执行需要权限的操作
    UI->>Agent: 发送工具调用请求
    Agent->>NodeACP: 请求权限(RequestPermissionRequest)
    NodeACP->>RPC: $showPermissionDialog(权限参数)
    RPC->>PermBridge: showPermissionDialog()
    PermBridge->>UI: 显示权限对话框
    UI->>User: 显示权限确认
    User->>UI: 选择允许/拒绝
    UI->>PermBridge: handleUserDecision()
    PermBridge->>RPC: 返回决策
    RPC->>NodeACP: 返回AcpPermissionDecision
    NodeACP->>Agent: RequestPermissionResponse(允许/拒绝)
    Agent->>Agent: 继续执行或中止
Loading
sequenceDiagram
    participant ChatUI as Chat UI
    participant ChatMgr as ChatManager<br/>(会话管理)
    participant Registry as SessionProvider<br/>Registry(注册表)
    participant LocalProvider as LocalStorage<br/>Provider
    participant ACPProvider as ACP<br/>Provider
    participant Backend as 后端服务

    ChatUI->>ChatMgr: init()
    ChatMgr->>Registry: 选择主提供者
    alt ACP模式
        ChatMgr->>ACPProvider: loadSessions()
        ACPProvider->>Backend: aiBackService.listSessions()
        Backend-->>ACPProvider: 会话列表
        ACPProvider-->>ChatMgr: ISessionModel[]
    else 本地模式
        ChatMgr->>LocalProvider: loadSessions()
        LocalProvider->>LocalProvider: 从localStorage读取
        LocalProvider-->>ChatMgr: ISessionModel[]
    end
    ChatMgr-->>ChatUI: 会话已加载

    ChatUI->>ChatMgr: startSession()
    alt ACP模式
        ChatMgr->>ACPProvider: createSession()
        ACPProvider->>Backend: aiBackService.createSession()
        Backend-->>ACPProvider: sessionId
        ACPProvider-->>ChatMgr: ISessionModel
    else 本地模式
        ChatMgr->>ChatMgr: 创建本地ChatModel
        ChatMgr-->>ChatMgr: 缓存
    end
    ChatMgr-->>ChatUI: 会话已创建
Loading

代码审查工作量评估

🎯 5 (Critical) | ⏱️ ~120 minutes

本PR涉及高度复杂的多层级集成:

  • ACP权限系统的完整实现(权限请求/响应/UI对话框),包含事件驱动和超时管理
  • Chat会话管理系统的架构重构(从单存储改为多提供者模式),影响所有会话操作
  • CLI代理进程生命周期的完整管理实现
  • Node端ACP客户端、代理服务、文件/终端处理器的多个复杂服务
  • 大量新公共API表面(权限对话框、会话提供者、ACP服务)
  • 浏览器和Node端的广泛DI配置变更
  • 高密度的业务逻辑(权限规则匹配、流式更新处理、进程管理)
  • 涉及多个独立的关键流程(权限流、会话流、代理通信流)

可能相关的PR

建议标签

🎨 feature, 🔧 refactor, test 📝

建议审查者

  • ensorrow
  • Ricbet
  • life2015
✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/acp

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
packages/ai-native/src/browser/chat/chat-multi-diff-source.ts (1)

101-103: ⚠️ Potential issue | 🟠 Major

使用 Event.signal() 消除双重断言的类型隐患。

Line 102 的 as unknown as Event<void> 双重断言会掩盖类型不匹配,应改用框架提供的事件适配工具。由于 onDidChange 的消费方不需要 CodeBlockData 的具体值,仅关心更新事件本身,最简洁的方案是使用 Event.signal() 直接将事件转换为 Event<void>

建议修改示例
-      // 这里event类型错误不影响
-      onDidChange: this.baseApplyService.onCodeBlockUpdate as unknown as Event<void>,
+      // 使用 Event.signal 将 Event<CodeBlockData> 转换为 Event<void>
+      onDidChange: Event.signal(this.baseApplyService.onCodeBlockUpdate),

若需保留事件消息但仅转发信号,也可用 Event.map() 的替代方案。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/chat/chat-multi-diff-source.ts` around lines
101 - 103, The current assignment uses a double-cast "as unknown as Event<void>"
for onDidChange; replace that unsafe cast by adapting
baseApplyService.onCodeBlockUpdate via the framework event helper: use
Event.signal(this.baseApplyService.onCodeBlockUpdate) to produce an Event<void>
(or use Event.map if you need to preserve payload while only forwarding a
signal). Update the onDidChange assignment to call Event.signal with the source
event (baseApplyService.onCodeBlockUpdate) instead of the double assertion.
packages/ai-native/src/browser/layout/ai-layout.tsx (1)

25-42: ⚠️ Potential issue | 🟠 Major

将 Hook 调用移到条件返回之前,确保 Hook 顺序一致

Line 25-36 的条件返回导致 Line 39-42 的 useMemo 只在 shouldShowFullLayout 为 true 时执行。这违反了 React Hooks 规则:Hook 必须在每次渲染时以相同的顺序调用,不能在条件分支中。当 shouldShowFullLayout 的值在两次渲染间变化时,React 会抛出运行时错误。

建议:将 defaultRightSize 的初始化移到条件返回之前,或改用简单赋值替代 useMemo(因为 designLayoutConfig 通常不会改变)。

建议修改
  // 判断是否应该显示完整布局
  const shouldShowFullLayout = !isMobileDevice();

+  // 正常模式:渲染完整布局
+  const defaultRightSize = designLayoutConfig.useMergeRightWithLeftPanel ? 0 : 49;
+
  // 移动端模式:只渲染 AI_CHAT_VIEW_ID,添加 mobile class
  if (!shouldShowFullLayout) {
    return (
      <SlotRenderer
        slot={AI_CHAT_VIEW_ID}
        isTabbar={true}
        defaultSize={layout['AI-Chat']?.currentId ? layout['AI-Chat']?.size || 360 : 0}
        maxResize={420}
        minResize={280}
        minSize={0}
      />
    );
  }
-
-  // 正常模式:渲染完整布局
-  const defaultRightSize = useMemo(
-    () => (designLayoutConfig.useMergeRightWithLeftPanel ? 0 : 49),
-    [designLayoutConfig.useMergeRightWithLeftPanel],
-  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/layout/ai-layout.tsx` around lines 25 - 42,
The useMemo hook for defaultRightSize is called only when shouldShowFullLayout
is true, violating React Hooks order; move the useMemo call for defaultRightSize
(or replace it with a plain calculation) above the conditional return that
renders SlotRenderer so that useMemo/designLayoutConfig is invoked on every
render; ensure references to defaultRightSize, useMemo, designLayoutConfig,
shouldShowFullLayout, SlotRenderer and AI_CHAT_VIEW_ID remain consistent after
moving.
packages/ai-native/src/browser/chat/chat-edit-resource.ts (1)

52-56: ⚠️ Potential issue | 🟡 Minor

补齐 side/id 参数校验,避免错误查询默认落到右侧内容。

当前当 sideleft(含缺失/非法)时会直接走 updatedCode 分支,可能掩盖无效 URI 并返回错误内容。建议仅接受 left/right,否则返回空串。

🔧 建议修改
-  async provideEditorDocumentModelContent(uri: URI, encoding?: string | undefined): Promise<string> {
+  async provideEditorDocumentModelContent(uri: URI, encoding?: string | undefined): Promise<string> {
     // Get the content from the base apply service based on the uri query parameters
-    const { id, side } = uri.getParsedQuery();
+    const { id, side } = uri.getParsedQuery() as { id?: string; side?: string };
+    if (!id || (side !== 'left' && side !== 'right')) {
+      return '';
+    }
     const codeBlocks = this.baseApplyService.getSessionCodeBlocks();
     const codeBlock = codeBlocks?.find((block) => block.toolCallId === id);
-    const content = side === 'left' ? codeBlock?.originalCode : codeBlock?.updatedCode;
-    return content || '';
+    return side === 'left' ? (codeBlock?.originalCode ?? '') : (codeBlock?.updatedCode ?? '');
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/chat/chat-edit-resource.ts` around lines 52 -
56, The current logic uses side !== 'left' to select updatedCode which causes
invalid/missing side or id to default to the right side; update the code around
uri.getParsedQuery(), baseApplyService.getSessionCodeBlocks(), and the codeBlock
lookup to strictly validate both id and side: ensure id is present, only accept
side === 'left' or side === 'right', return '' immediately for any other values,
find the codeBlock by toolCallId === id and if not found return '', and only
then return codeBlock.originalCode for 'left' or codeBlock.updatedCode for
'right'.
packages/ai-native/src/browser/ai-core.contribution.ts (1)

301-310: ⚠️ Potential issue | 🟠 Major

不要把 chatManagerService.init() 变成未处理的后台任务。

Line 309 去掉 await 后,初始化失败会变成未处理 rejection,会话恢复也会和后续依赖方产生竞态。除非这里已经有独立的 readiness 机制,否则应继续等待它完成;如果是有意异步化,也至少要显式 catch

建议修改
-      this.chatManagerService.init();
+      await this.chatManagerService.init();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/ai-core.contribution.ts` around lines 301 -
310, The call to chatManagerService.init() inside initialize is now
fire-and-forget and can produce unhandled rejections and race conditions; change
it to await chatManagerService.init() so initialize waits for completion (or, if
intentional, explicitly handle errors by adding a .catch(...) that logs/fails
gracefully and exposes readiness) — update the initialize method to either await
chatManagerService.init() or wrap chatManagerService.init() with an explicit
catch that surfaces errors; locate this in the initialize function alongside
chatInternalService.init() and chatProxyService.registerDefaultAgent() and
ensure AI_CHAT_VIEW_ID / AI_CHAT_CONTAINER_ID layout registration remains
unchanged.
packages/ai-native/src/browser/chat/chat.view.tsx (1)

815-829: ⚠️ Potential issue | 🟠 Major

Line 816 的非空断言无法防止从 storage 恢复时 request 为 undefined 导致的崩溃

Line 816 先使用可选链 sessionModel?.getRequest(...) 再用非空断言 ! 掩盖,但 renderReply 函数签名中 request: ChatRequestModel 是必需参数。当从 storage 恢复的 assistant 消息缺少对应的 request 时,非空断言会让 undefined 通过类型检查,随后在 renderReply 内部访问 request.requestId 时触发运行时异常。

需要移除非空断言并添加空值检查,当 request 为 undefined 时调用 renderSimpleMarkdownReply 作为后备方案:

🔧 建议修改
-          const request = aiChatService.sessionModel?.getRequest(msg.requestId)!;
+          const request = aiChatService.sessionModel?.getRequest(msg.requestId);
           // 从storage恢复时,request为undefined
           if (request && !request.response.isComplete) {
             setLoading(true);
           }
-          await renderReply({
-            msgId: msg.id,
-            relationId: msg.relationId!,
-            message: msg.content,
-            agentId: msg.agentId,
-            command: msg.agentCommand,
-            startTime: msg.replyStartTime!,
-            request,
-          });
+          if (request) {
+            await renderReply({
+              msgId: msg.id,
+              relationId: msg.relationId!,
+              message: msg.content,
+              agentId: msg.agentId,
+              command: msg.agentCommand,
+              startTime: msg.replyStartTime!,
+              request,
+            });
+          } else if (msg.content) {
+            await renderSimpleMarkdownReply({
+              relationId: msg.relationId!,
+              chunk: msg.content,
+            });
+          }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/chat/chat.view.tsx` around lines 815 - 829,
The code uses a non-null assertion on the result of
aiChatService.sessionModel?.getRequest(msg.requestId) (the local variable
request) but renderReply requires a ChatRequestModel and will throw if request
is actually undefined during storage restore; remove the trailing "!" non-null
assertion, add an explicit check for request before calling renderReply, and
when request is undefined call renderSimpleMarkdownReply with the assistant
message (preserving relationId/agentId/command/startTime as appropriate); also
keep the existing setLoading logic only when request exists and
!request.response.isComplete so you don't set loading for the fallback path.
🟠 Major comments (30)
packages/core-common/src/log.ts-214-216 (1)

214-216: ⚠️ Potential issue | 🟠 Major

恢复可控的 debug 开关,当前改动会让调试日志永久关闭

Line 215 被注释后,this.isEnable 在当前类中不再有任何赋值为 true 的路径,导致 verbose/debug/log/warn/info 全部不可达(仅 error 还能输出)。这会显著削弱排障能力。

建议修复
   constructor(namespace?: string) {
     if (typeof process !== 'undefined' && process.env && process.env.KTLOG_SHOW_DEBUG) {
-      // this.isEnable = true;
+      this.isEnable = true;
     }

     this.namespace = namespace || '';
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core-common/src/log.ts` around lines 214 - 216, The debug switch was
effectively disabled by commenting out the assignment so this.isEnable never
becomes true; restore the conditional that sets this.isEnable = true when
process.env.KTLOG_SHOW_DEBUG is present (check the environment in the same block
using typeof process !== 'undefined' and process.env) so the class-level flag
(this.isEnable) can re-enable verbose/debug/log/warn/info methods (only error
currently reachable); update the block that references
process.env.KTLOG_SHOW_DEBUG to assign this.isEnable = true and keep the
surrounding safety checks intact.
packages/ai-native/src/browser/acp/permission.handler.ts-60-68 (1)

60-68: ⚠️ Potential issue | 🟠 Major

权限存储的异步初始化没有被等待。

Line 60-68 在构造函数里 fire-and-forget initStorage(),但后面的 requestPermission() / saveRules() 已经假定 storage 可用了。首个请求如果来得够快,已存规则不会生效,用户点 “always” 之后的新规则也可能直接丢掉。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/acp/permission.handler.ts` around lines 60 -
68, The constructor calls initStorage() without waiting, so permissionStorage
may be undefined when requestPermission() or saveRules() run; update the class
to ensure initialization is awaited by creating and storing an initialization
Promise (e.g., this.initPromise = this.initStorage()) or by making initStorage
set a ready Promise, then await that Promise at the start of requestPermission()
and saveRules() before accessing permissionStorage; reference the existing
symbols: constructor, initStorage(), permissionStorage,
storageProvider(STORAGE_NAMESPACE.AI_NATIVE), loadRules(), requestPermission(),
and saveRules() so the methods reliably wait for storage to be initialized.
packages/ai-native/src/browser/acp/permission.handler.ts-117-120 (1)

117-120: ⚠️ Potential issue | 🟠 Major

“始终允许/拒绝” 现在保存的是错误的 rule 维度。

Line 119 把 optionId 传给 addRule(),而 Line 260-278 又把它直接当成 pattern 存下来,并把 kind 固定成 'write'。结果保存出来的 rule 类似 allow_always => allow,后续 checkRules() 用真实路径/标题去匹配时永远命中不到,read/command 规则也都会被误归类。

建议修改
  private pendingRequests = new Map<
    string,
    {
      resolve: (decision: PermissionDecision) => void;
      timeout: NodeJS.Timeout;
+     request: PermissionRequest;
    }
  >();
...
      this.pendingRequests.set(requestId, {
        resolve,
        timeout,
+       request,
      });
...
    if (always) {
-     this.addRule(requestId, optionId, allow ? 'allow' : 'reject');
+     this.addRule(pending.request, allow ? 'allow' : 'reject');
    }
...
- private addRule(requestId: string, pattern: string, decision: 'allow' | 'reject'): void {
-   // Extract pattern from request
-   // This is a placeholder - actual implementation should extract from the request
+ private addRule(request: PermissionRequest, decision: 'allow' | 'reject'): void {
+   const pattern =
+     request.toolCall.locations?.length
+       ? request.toolCall.locations.map((l) => l.path).join(',')
+       : request.toolCall.title || '';
+
    const rule: PermissionRule = {
      id: uuid(),
      pattern,
-     kind: 'write', // Should be extracted from actual request
+     kind: request.toolCall.kind || 'read',
      decision,
      always: true,
      createdAt: Date.now(),
    };

Also applies to: 260-278

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/acp/permission.handler.ts` around lines 117 -
120, The "always allow/reject" branch currently calls addRule(requestId,
optionId, ...) and the storage logic (around the addRule implementation and the
block at the 260-278 region) treats optionId as the rule pattern and hardcodes
kind='write', which produces rules like "allow_always => allow" that never match
in checkRules() and misclassifies read/command rules; change addRule calls and
the rule-persistence code so that the stored rule uses the actual resource
pattern/descriptor (e.g., the request's path/title or the resolved option
pattern) and the correct kind (read/command/write) derived from the original
request/option, not the optionId string; update addRule(requestId, optionId,
...) usage to pass the resolved pattern and kind (or have addRule resolve them
from requestId/optionId) and ensure checkRules() matching will then succeed.
packages/ai-native/src/browser/chat/chat-model.ts-315-329 (1)

315-329: ⚠️ Potential issue | 🟠 Major

title 新增后还没有进入持久化。

Line 317-329 把标题加进了构造参数和 getter,但 toJSON() 仍然没带上它。只要会话经过 storage/provider 恢复,标题就会丢失,这个 session 标题能力就只在当前内存实例里有效。

建议修改
  toJSON() {
    return {
      sessionId: this.sessionId,
+     title: this.title,
      modelId: this.modelId,
      history: this.history,
      requests: this.requests,
    };
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/chat/chat-model.ts` around lines 315 - 329,
The new private field `#title` and its getter are not persisted because toJSON()
does not include the title, so session restores lose it; update the ChatModel's
serialization to include title (e.g., add title: this.#title to the object
returned by toJSON()) and ensure any static fromJSON/restore factory (or
constructor usage when deserializing) reads that title back into `#title` so
restored sessions preserve the title; reference the constructor, `#title/title`
getter and toJSON()/fromJSON() (or restore method) to locate where to add the
write/read of the title.
packages/ai-native/__test__/node/acp/cli-agent-process-manager.test.ts-40-46 (1)

40-46: ⚠️ Potential issue | 🟠 Major

这里把 cwd 写死成 /tmp,会让用例在非 POSIX 环境直接失败。

如果 CI 覆盖 Windows,这个断言路径不存在。这里最好改成 os.tmpdir() 或测试夹具目录。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/__test__/node/acp/cli-agent-process-manager.test.ts`
around lines 40 - 46, The test hardcodes '/tmp' as the cwd which breaks on
non-POSIX systems; update the second startAgent call in the test (in
cli-agent-process-manager.test.ts) to use a cross-platform temp directory
instead of '/tmp' — for example call processManager.startAgent('node', ['-e',
'setInterval(() => {}, 1000)'], {}, os.tmpdir()) or use the test fixture/tmpDir
helper if available; ensure you import/require os (or the fixture) at the top so
the test runs on Windows CI as well.
packages/ai-native/src/node/acp/acp-cli-client.service.ts-216-234 (1)

216-234: ⚠️ Potential issue | 🟠 Major

挂起请求现在既不会超时,也不会在 close() 时被 reject。

requestTimeoutMs 定义了但没用;close() 也没有像 handleDisconnect() 那样清空 pendingRequests。只要 agent 漏一条响应,或者关闭发生在请求飞行中,调用方 Promise 就会永久悬挂。

Also applies to: 248-270

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/node/acp/acp-cli-client.service.ts` around lines 216 -
234, The close() method currently clears connection state but neither rejects
pendingRequests nor uses requestTimeoutMs, causing in-flight requests to hang;
update close() (and mirror the logic used in handleDisconnect()) to iterate over
this.pendingRequests and reject each Promise with an appropriate Error (e.g.,
"connection closed"), then clear the pendingRequests map/array; additionally,
ensure the request-sending code (e.g., the method that registers entries in
this.pendingRequests) honors this.requestTimeoutMs by starting a timer per
request that rejects and cleans up the pendingRequests entry on timeout so
callers never hang (apply same fix pattern to the other close-like block around
the 248-270 region).
packages/ai-native/src/node/acp/acp-cli-client.service.ts-250-253 (1)

250-253: ⚠️ Potential issue | 🟠 Major

断连后仍然可能继续向旧流写数据。

handleDisconnect() 只把 connected 置成 false,但保留了旧的 stdin / stdout 引用;而发送路径只检查 this.stdin 是否非空。断连后的新请求不会立即失败,而是继续写向失效 transport。

Also applies to: 273-281, 433-454

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/node/acp/acp-cli-client.service.ts` around lines 250 -
253, The code allows writes to stale stdin/stdout after disconnect because
handleDisconnect() only sets connected=false but leaves this.stdin/this.stdout
references intact and sendRequest() only checks this.stdin non-null; update
handleDisconnect() (and any disconnect handlers referenced around sendRequest,
the logic at lines near 273-281 and 433-454) to null out/clear this.stdin and
this.stdout (or replace with a closed stream/erroring stub) and/or make
sendRequest() validate this.connected in addition to this.stdin before writing,
so new requests fail fast; ensure any send paths (e.g., sendRequest<T>) re-check
this.connected and throw immediately if disconnected to prevent writing to an
invalid transport.
packages/ai-native/src/browser/acp/permission-dialog-container.module.less-1-13 (1)

1-13: ⚠️ Potential issue | 🟠 Major

这个容器现在不是模态的。

根节点 pointer-events: none 会让弹窗外区域继续把点击落到下面的页面。对权限确认这种交互来说,这会破坏模态语义,也容易在授权过程中触发背景操作。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/acp/permission-dialog-container.module.less`
around lines 1 - 13, 根节点 .dialogContainer 现在使用 pointer-events: none
导致容器不是模态的——点击会穿透到页面底层;把根容器改为能拦截事件(移除 pointer-events: none 或改为 pointer-events:
auto)以确保弹窗外区域不会把点击传递到背景,并在需要时在 .dialogContainer
内添加一个覆盖层(半透明背景)用于捕获和处理外部点击(例如用于取消或阻止点击),同时保留对弹窗内容的点击(目前的 > *
规则可按需保留或调整以确保子元素正常接收事件)。
packages/ai-native/src/node/acp/acp-cli-client.service.ts-257-269 (1)

257-269: ⚠️ Potential issue | 🟠 Major

不要把 ACP 原始 payload 直接打到日志里。

这里会记录完整 params / message 片段,里面很容易带上用户 prompt、文件内容、认证信息或命令参数。默认日志级别下直接落盘,隐私和合规风险都太高。

Also applies to: 296-304

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/node/acp/acp-cli-client.service.ts` around lines 257 -
269, The current logging in send request writes full params/JSON (in logger?.log
and logger?.debug) which may leak prompts, files, or credentials; update the
send flow (references: logger?.log call, logger?.debug call, pendingRequests
set, message/json creation, and stdin!.write) to avoid emitting raw payloads:
log only non-sensitive metadata such as method and id (and optionally param keys
without values), or replace params/json with a fixed placeholder like
"[REDACTED]" at info/default levels; if you need payload visibility keep it
behind a debug/trace-only sanitized formatter that strips values (or truncates
and hashes) before logging, and ensure the code paths around the message/json
and logger calls (including the similar block at lines 296-304) are changed
accordingly.
packages/ai-native/src/browser/chat/session-provider-registry.ts-83-94 (1)

83-94: ⚠️ Potential issue | 🟠 Major

重复注册时,旧 disposer 会误删新的 provider。

这里返回的 dispose() 只按 provider.id 删除。先注册 A、再用同一 id 注册 B 之后,如果 A 的 disposer 晚一点执行,会把 B 也一起删掉。

建议修复
     return {
       dispose: () => {
-        this.providers.delete(provider.id);
+        if (this.providers.get(provider.id) === provider) {
+          this.providers.delete(provider.id);
+        }
       },
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/chat/session-provider-registry.ts` around
lines 83 - 94, The dispose returned by registerProvider currently deletes by
provider.id and can remove a newer provider with the same id; change it to
capture the registered instance (or a unique token) at registration time and, in
the returned dispose(), only delete this.providers.delete(provider.id) if
this.providers.get(provider.id) === capturedInstance (or token matches); update
registerProvider (and the IDisposable returned) so the disposer validates the
stored instance before removing to avoid deleting a newer provider registered
under the same id.
packages/ai-native/src/node/acp/cli-agent-process-manager.ts-202-207 (1)

202-207: ⚠️ Potential issue | 🟠 Major

当前的进程组杀死逻辑存在缺陷,无法彻底清理子进程树。

代码尝试用 process.kill(-pid) 杀死进程组,但子进程配置为 detached: false。在这种情况下,子进程会继承父进程的进程组ID,而不是成为自己进程组的leader。因此 process.kill(-pid) 的负PID操作会失败(ESRCH错误),虽然代码确实有fallback逻辑(第270行捕获异常后改用单进程kill),但单进程kill只会清理直接子进程,无法杀死孙进程及更深层级的子进程。

如果需要可靠地控制整个进程树,应该考虑使用 detached: true 创建新的进程组。虽然代码注释说明了选择 detached: false 是为了等待子进程退出,但实际上即使设置 detached: true,仍可通过监听 exit 事件正常等待进程退出,不会影响现有的等待逻辑。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/node/acp/cli-agent-process-manager.ts` around lines
202 - 207, The current child spawn uses detached: false so process.kill(-pid)
can't target a new process group; change spawn(...) to use detached: true so the
child becomes leader of its own process group, keep stdio as pipes and do NOT
call child.unref() so the parent can still wait; retain the existing exit/close
listeners (e.g., where the code currently awaits childProcess 'exit'/'close') to
wait for termination, and update the group-kill logic to call
process.kill(-childProcess.pid, signal) to terminate the whole group; keep the
existing fallback single-process kill and platform-safe handling (Windows
taskkill) for environments that don't support negative PIDs.
packages/ai-native/src/node/acp/handlers/terminal.handler.ts-149-160 (1)

149-160: ⚠️ Potential issue | 🟠 Major

输出截断状态现在是不可信的。

缓冲区超限后这里按字符 slice(),不是按字节裁剪;而 getTerminalOutput() 又靠“当前 buffer 是否仍然超限”来推断 truncated。结果就是旧输出明明已经被丢弃,返回仍可能是 false

Also applies to: 209-216

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/node/acp/handlers/terminal.handler.ts` around lines
149 - 160, The truncation logic uses string.slice() (character-based) and
doesn't set the truncated flag, causing getTerminalOutput() to incorrectly
report truncated=false; update the onData handler (and the similar block around
the other listener) to trim the buffer by bytes not characters: compute how many
bytes to keep relative to terminalSession.outputByteLimit, convert the buffer to
a byte-aware form (e.g., Buffer) to slice the tail by byte length, assign the
resulting utf8 string back to terminalSession.outputBuffer, and set a
terminalSession.truncated (or equivalent) boolean when any trimming occurs so
getTerminalOutput() can reliably detect truncation. Ensure you update both the
onData block that references terminalSession.outputBuffer and the repeated block
at lines ~209-216.
packages/ai-native/src/browser/chat/session-provider-registry.ts-119-121 (1)

119-121: ⚠️ Potential issue | 🟠 Major

getProviderBySessionId() 现在没有按 source 做路由。

这里把完整 sessionId 原样传给 getProvider(),但接口和注释约定的是基于 source 选择 provider。只要某个实现按 'local' | 'acp' 判断 canHandle(),这里就会直接找不到 provider。

建议修复
-import { ISessionProvider, SessionProviderDomain } from './session-provider';
+import { ISessionProvider, SessionProviderDomain, parseSessionId } from './session-provider';
@@
   getProviderBySessionId(sessionId: string): ISessionProvider | undefined {
-    const provider = this.getProvider(sessionId);
+    const { source } = parseSessionId(sessionId);
+    const provider = this.getProvider(source);
     return provider;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/chat/session-provider-registry.ts` around
lines 119 - 121, getProviderBySessionId currently forwards the full sessionId to
getProvider, but the registry should route by the session "source" (e.g.,
'local' | 'acp') so implementations that base canHandle() on source fail to
match; change getProviderBySessionId to extract the source from sessionId (e.g.,
parse by a delimiter such as ':' or use the established sessionId format to
obtain the source token) and then call getProvider(source) or iterate providers
and call provider.canHandle(source) to locate the correct ISessionProvider;
update getProviderBySessionId to return that provider instead of passing the
whole sessionId through.
packages/ai-native/src/browser/components/mention-input/mention-input.tsx-97-99 (1)

97-99: ⚠️ Potential issue | 🟠 Major

optionsBottomPosition 这段状态目前没有真正生效。

这里只初始化成 0,但整个文件里没有任何地方调用 setOptionsBottomPosition。因此 PermissionDialogWidget 会一直收到固定的 bottom={0},和 footer / selector 的实际高度脱钩。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/components/mention-input/mention-input.tsx`
around lines 97 - 99, The state optionsBottomPosition is never updated, so
PermissionDialogWidget always receives bottom={0}; add logic to measure the
actual footer/selector height and update setOptionsBottomPosition accordingly:
attach a ref to the footer/selector element (the DOM node that determines the
bottom offset), on mount measure its clientHeight and call
setOptionsBottomPosition(height) and also subscribe to changes via a
ResizeObserver (or window resize) to update it dynamically; ensure you cleanup
the observer on unmount. Update any JSX to pass the measured
optionsBottomPosition to PermissionDialogWidget and remove the hardcoded 0
usage.
packages/ai-native/src/browser/chat/chat.internal.service.ts-128-135 (1)

128-135: ⚠️ Potential issue | 🟠 Major

避免 clearSessionModel 先删后建留下半失效状态。

当前会先清理旧会话,再 await startSession()。如果新会话创建失败,服务会停在“旧会话已清理 / 新会话未建立”的中间态,而 #sessionModel 仍可能保留旧值。这里至少要在失败时显式清空/回退当前会话,并补上 loading 的生命周期。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/chat/chat.internal.service.ts` around lines
128 - 135, clearSessionModel currently clears the old session first then awaits
startSession, risking a half-invalid state if startSession fails; change it so
you either create the new session before removing the old one or perform a safe
swap with try/catch and explicit rollback/clearing: call
this._onWillClearSession, set this.#sessionModel to a "loading" or null
sentinel, then await this.chatManagerService.startSession() inside a try block,
on success replace `#sessionModel` and call
this.chatManagerService.clearSession(oldSessionId) and this._onChangeSession; on
failure ensure `#sessionModel` is explicitly null/cleared (or restored to a
previous safe state) and emit appropriate change/error lifecycle events so the
UI never sees “old cleared but new missing” state.
packages/ai-native/src/browser/chat/chat.internal.service.ts-86-95 (1)

86-95: ⚠️ Potential issue | 🟠 Major

不要在后端未实现时仍然广播“模式切换成功”。

这里用了可选调用。setSessionMode 缺失时,await 会直接得到 undefined,但 _onModeChange.fire(modeId) 仍然会执行。UI 会以为模式已经切走,后端实际却没变。

🛠️ 建议修改
   async setSessionMode(modeId: string): Promise<void> {
     const sessionId = this.#sessionModel?.sessionId;
     if (!sessionId) {
       throw new Error('No active session');
     }

-    await this.aiBackService.setSessionMode?.(sessionId, modeId);
+    if (!this.aiBackService.setSessionMode) {
+      throw new Error('setSessionMode is not supported by the current backend');
+    }
+    await this.aiBackService.setSessionMode(sessionId, modeId);
     // 切换成功后通知前端 UI 同步更新当前模式
     this._onModeChange.fire(modeId);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/chat/chat.internal.service.ts` around lines 86
- 95, The method setSessionMode currently always fires
_onModeChange.fire(modeId) even when aiBackService.setSessionMode is missing
(optional) and therefore does nothing; change setSessionMode to first verify
aiBackService.setSessionMode exists and await its call (or capture its result)
and only call this._onModeChange.fire(modeId) when the backend call actually
executed successfully (or the function exists and resolved without error); if
aiBackService.setSessionMode is undefined, either throw or return without firing
to avoid informing the UI of a non-existent backend change (refer to the
setSessionMode method and aiBackService.setSessionMode and _onModeChange.fire
symbols).
packages/ai-native/src/browser/chat/chat.internal.service.ts-72-77 (1)

72-77: ⚠️ Potential issue | 🟠 Major

onStorageInit 回调里补上 await this.createSessionModel()

现在回调已经是 async,但无会话分支仍然直接丢掉 Promise。startSession() 一旦失败会变成未处理拒绝,而且 storage init 会在会话真正建立前提前结束。

🛠️ 建议修改
       if (sessions.length > 0) {
         await this.activateSession(sessions[sessions.length - 1].sessionId);
       } else {
-        this.createSessionModel();
+        await this.createSessionModel();
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/chat/chat.internal.service.ts` around lines 72
- 77, The onStorageInit callback currently awaits activateSession when sessions
exist but does not await the Promise returned by createSessionModel() in the "no
sessions" branch, causing unhandled rejections if startSession() fails and
finishing storage init prematurely; update the onStorageInit handler so it
awaits this.createSessionModel() (i.e., replace the bare call with await
this.createSessionModel()) to ensure session creation completes before storage
init resolves and to propagate errors properly from
createSessionModel/startSession.
packages/ai-native/src/browser/chat/chat.internal.service.ts-119-125 (1)

119-125: ⚠️ Potential issue | 🟠 Major

createSessionModel 需要用 finally 回收 loading 状态。

startSession() 抛错时,Line 121 打开的 loading 永远不会关闭,界面会一直卡在 loading。

🛠️ 建议修改
   async createSessionModel() {
-    // this.__isSessionLoading =  true;
     this._onSessionLoadingChange.fire(true);
-    this.#sessionModel = await this.chatManagerService.startSession();
-    this._onChangeSession.fire(this.#sessionModel.sessionId);
-    // this.__isSessionLoading =  false;
-    this._onSessionLoadingChange.fire(false);
+    try {
+      this.#sessionModel = await this.chatManagerService.startSession();
+      this._onChangeSession.fire(this.#sessionModel.sessionId);
+    } finally {
+      this._onSessionLoadingChange.fire(false);
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/chat/chat.internal.service.ts` around lines
119 - 125, createSessionModel sets loading true then awaits
chatManagerService.startSession but never resets loading if startSession throws;
wrap the await in a try/finally so _onSessionLoadingChange.fire(false) always
runs, assign this.#sessionModel and fire _onChangeSession inside the try after
the await, and rethrow or let the error propagate after the finally so callers
still see failures.
packages/ai-native/src/node/acp/acp-cli-back.service.ts-261-299 (1)

261-299: ⚠️ Potential issue | 🟠 Major

恢复历史时不要把 chunk 直接当成完整消息。

这里把 user_message_chunk / agent_message_chunk 每条通知都直接 push 成一条消息了。会话恢复后,同一条用户消息或 agent 回复会被切成很多碎片消息,后面的 summary、follow-up 和上下文窗口都会错位。至少要先把连续 chunk 合并成完整 message,再返回给上层。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/node/acp/acp-cli-back.service.ts` around lines 261 -
299, convertSessionUpdatesToMessages currently treats each
user_message_chunk/agent_message_chunk as a full message, causing fragmented
messages after restoration; change it to buffer and merge consecutive chunk
notifications for the same logical message by concatenating update.content.text
(preserve order) and using the first/earliest timestamp, then push a single
{role, content, timestamp} when the sequence ends (detect end by a non-chunk
update or a boundary change in role/session id/sequence indicator on
notification.update); update logic in convertSessionUpdatesToMessages to
accumulate chunks (for both 'user_message_chunk' and 'agent_message_chunk') and
flush the combined message instead of pushing every chunk separately.
packages/ai-native/src/browser/components/permission-dialog-widget.tsx-35-41 (1)

35-41: ⚠️ Potential issue | 🟠 Major

Enter 键读取过期的焦点索引

keydown 事件监听在 effect 中注册,但依赖数组仅包含 [dialogs.length, dialogs],没有 focusedIndex。当用户按 ArrowUp/ArrowDown 改变焦点时,focusedIndex 状态会更新,但 effect 不会重新运行,导致 handleKeyboard 仍然持有旧闭包中的 focusedIndex。虽然 ArrowUp/ArrowDown 通过函数式 setState 能正常工作,但第 59 行 Enter 键读取的 focusedIndex 仍是过期值,会错误提交。

建议:将 focusedIndex 加入依赖数组,或用 useCallback 包装 handleKeyboard 并依赖 focusedIndex,或用 ref 保存最新值。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/components/permission-dialog-widget.tsx`
around lines 35 - 41, The keydown handler (handleKeyboard) is capturing a stale
focusedIndex because the effect that registers it only depends on
[dialogs.length, dialogs]; update the effect so the listener always sees the
latest focusedIndex by either adding focusedIndex to the useEffect dependency
array, or memoizing handleKeyboard with useCallback that lists focusedIndex in
its deps, or storing focusedIndex in a ref and reading from that ref inside
handleKeyboard; locate handleKeyboard, the useEffect that adds/removes the
'keydown' listener, and the focusedIndex state to apply one of these fixes.
packages/ai-native/src/browser/acp/permission-dialog.view.tsx-40-61 (1)

40-61: ⚠️ Potential issue | 🟠 Major

倒计时在新请求或重新打开时不会重置。

remainingTime 只在首次挂载时用 timeout 初始化,不会在后续变化时重置。当组件被复用到新的 requestIdvisible 再次变为 true 时,会继承之前的倒计时状态(通常是 0)。由于倒计时效果在第 45 行检查 if (!visible || remainingTime <= 0),如果状态未重置,新弹窗会立即满足退出条件,导致 auto-reject。

🔧 建议修改
   const [remainingTime, setRemainingTime] = useState(timeout);
+
+  useEffect(() => {
+    if (visible) {
+      setRemainingTime(timeout);
+    }
+  }, [visible, timeout, requestId]);
 
   // Countdown timer
   useEffect(() => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/acp/permission-dialog.view.tsx` around lines
40 - 61, The countdown state remainingTime is only initialized from timeout once
and never reset for new requests or when visible toggles, causing immediate
auto-reject; add an effect that resets remainingTime to timeout whenever visible
becomes true or requestId changes (e.g. useEffect(() => { if (visible)
setRemainingTime(timeout); }, [visible, requestId, timeout])) and keep the
existing countdown effect (which references remainingTime, visible, requestId,
onClose) so the timer restarts correctly for each new request.
packages/ai-native/src/browser/components/ChatMentionInput.tsx-450-455 (1)

450-455: ⚠️ Potential issue | 🟠 Major

useMemo 依赖数组不完整

defaultMentionInputFooterOptions 使用了 modeOptionscurrentMode,但它们没有被包含在依赖数组中。这会导致当 mode 相关状态变化时,footer 配置不会更新。

🔧 建议修复
-    [iconService, handleShowMCPConfig, handleShowRules, props.disableModelSelector, props.sessionModelId],
+    [iconService, handleShowMCPConfig, handleShowRules, props.disableModelSelector, props.sessionModelId, modeOptions, currentMode],

Also applies to: 530-530

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/components/ChatMentionInput.tsx` around lines
450 - 455, The useMemo that computes defaultMentionInputFooterOptions (type
FooterConfig) is missing dependencies, so changes to modeOptions or currentMode
won't update the memoized value; update the dependency array of the useMemo that
defines defaultMentionInputFooterOptions to include modeOptions and currentMode
(and any other values used inside like modeOptions[0]) so the footer config
recomputes when those values change—look for the useMemo that returns {
modeOptions, defaultMode: modeOptions[0]?.id || 'default', currentMode,
showModeSelector: modeOptions.length > 1 } and add modeOptions and currentMode
to its dependency list.
packages/ai-native/src/browser/acp/permission-dialog-container.tsx-238-245 (1)

238-245: ⚠️ Potential issue | 🟠 Major

handleDialogClose 也需要通知 bridge service

关闭对话框时应该通知 permissionBridgeService 用户取消了操作,否则后端会一直等待响应。

🔧 建议修复
   // 处理对话框关闭
   const handleDialogClose = useCallback(() => {
     if (dialogs.length === 0) {
       return;
     }
     const requestId = dialogs[0].requestId;
+    // 通知 bridge service 用户取消了操作
+    permissionBridgeService.handleDialogClose(requestId);
     functionComponentDialogManager.removeDialog(requestId);
-  }, [dialogs]);
+  }, [dialogs, permissionBridgeService]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/acp/permission-dialog-container.tsx` around
lines 238 - 245, The dialog close handler (handleDialogClose) currently only
removes the dialog via functionComponentDialogManager.removeDialog(requestId)
and does not inform the backend; update handleDialogClose to also call the
permissionBridgeService cancellation notification with the same requestId (e.g.,
permissionBridgeService.notifyDialogCancelled(requestId) or the appropriate
cancel method provided by permissionBridgeService) so the backend stops waiting
for a response, and guard the call if permissionBridgeService is undefined; keep
the removeDialog call and ensure the notification runs before or immediately
after removing the dialog.
packages/ai-native/src/browser/index.ts-203-206 (1)

203-206: ⚠️ Potential issue | 🟠 Major

DefaultChatAgentToken 的绑定和模式分支不一致。

ChatAgentPromptProvider 已经按 supportsAgentMode 分支,但这里始终把 DefaultChatAgentToken 绑定到 AcpChatAgent。如果本地模式还需要保留,这里会让非 ACP 场景也解析到 ACP 实现。

Also applies to: 232-239

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/index.ts` around lines 203 - 206,
DefaultChatAgentToken is always bound to AcpChatAgent which conflicts with
ChatAgentPromptProvider's supportsAgentMode branching and forces non-ACP
scenarios to resolve to the ACP implementation; change the binding so
DefaultChatAgentToken resolves conditionally based on supportsAgentMode (the
same condition used by ChatAgentPromptProvider) and preserve the local-mode
binding (e.g., map DefaultChatAgentToken to the local implementation when
supportsAgentMode is false); update both binding sites (the one shown and the
similar block around lines 232-239) to use the conditional/resolution logic so
non-ACP flows get the correct agent implementation.
packages/ai-native/src/node/acp/acp-agent.service.ts-483-485 (1)

483-485: ⚠️ Potential issue | 🟠 Major

权限弹窗和取消请求都可能落到错误的 session 上。

requestPermission() 取的是 this.sessionInfo?.sessionId,拒绝工具调用时也取消 this.sessionInfo.sessionId。但真正触发 tool call 的是当前通知对应的 notification.sessionId。一旦存在多 session,或者 createSession() 后还没更新 sessionInfo,这里就会把权限请求/取消发到别的会话甚至空 session。

🔁 建议修复

把当前通知的 sessionId 一路透传到权限请求和取消路径,例如:

-  async requestPermission(toolCallUpdate: ToolCallUpdate): Promise<PermissionResult> {
+  async requestPermission(sessionId: string, toolCallUpdate: ToolCallUpdate): Promise<PermissionResult> {
     const request: RequestPermissionRequest = {
-      sessionId: this.sessionInfo?.sessionId || '',
+      sessionId,
-        this.handleToolCallWithPermission(update, stream, pendingToolCalls);
+        this.handleToolCallWithPermission(notification.sessionId, update, stream, pendingToolCalls);
-      const result = await this.requestPermission(toolCallUpdate);
+      const result = await this.requestPermission(sessionId, toolCallUpdate);
...
-          await this.cancelRequest(this.sessionInfo.sessionId);
+          await this.cancelRequest(sessionId);

Also applies to: 511-527, 642-674

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/node/acp/acp-agent.service.ts` around lines 483 - 485,
The permission popup and cancel actions use this.sessionInfo?.sessionId, which
can target the wrong or empty session when multiple sessions exist or
sessionInfo is stale; update the flow so the notification's sessionId is passed
through to requestPermission and any cancel logic: modify
handleToolCallWithPermission (and related code paths referenced around lines
511-527 and 642-674) to accept an explicit sessionId argument (use
notification.sessionId) and propagate that sessionId into requestPermission(),
the rejection/cancel handler, and any createSession-related callbacks instead of
reading this.sessionInfo; ensure requestPermission() signature and callers
accept the sessionId parameter and that cancel actions use the same sessionId
value.
packages/ai-native/src/browser/acp/permission-bridge.service.ts-124-145 (1)

124-145: ⚠️ Potential issue | 🟠 Major

cancelRequest() 现在会把取消态错误地上报成 timeout

cancelRequest() 直接复用了 handleDialogClose(),而 handleDialogClose() 固定返回 { type: 'timeout' }。这样上游拿不到真实的取消结果,取消和超时会被混成一种状态。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/acp/permission-bridge.service.ts` around lines
124 - 145, The cancelRequest currently reuses handleDialogClose which always
produces a PermissionDecision of { type: 'timeout' }, so cancelled requests are
misreported as timeouts; change cancelRequest to produce and dispatch a distinct
cancel decision (e.g., { type: 'cancel' } or { type: 'user_cancel' }) and
perform the same cleanup (clearTimeout, delete from pendingDecisions and
activeDialogs, fire onPermissionResult, resolve/reject the pending promise) or
refactor the shared cleanup into a helper like finalizeRequest(requestId,
decision) that both handleDialogClose and cancelRequest call to ensure correct
decision is reported.
packages/ai-native/src/browser/chat/chat-manager.service.ts-117-134 (1)

117-134: ⚠️ Potential issue | 🟠 Major

保存会话时把 title 丢掉了。

fromJSON() 已经会恢复 item.title,但这里序列化时没有写回去。只要触发一次 saveSessions(),已有标题就会被覆盖掉。

💾 建议修复
   private toSessionData(model: ChatModel): ISessionModel {
     return {
       sessionId: model.sessionId,
       modelId: model.modelId,
+      title: model.title,
       history: model.history.toJSON(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/chat/chat-manager.service.ts` around lines 117
- 134, toSessionData currently omits the session title so saving sessions
overwrites existing titles; update the toSessionData method to include the title
field (e.g., add title: model.title) in the returned ISessionModel object so
fromJSON() can restore item.title correctly; locate the toSessionData function
in chat-manager.service.ts and append the title property to the returned object
alongside sessionId, modelId, history, and requests.
packages/ai-native/src/browser/chat/chat-manager.service.ts-160-164 (1)

160-164: ⚠️ Potential issue | 🟠 Major

预加载进缓存的 session 没有挂上自动保存监听。

startSession()getSession() 在放入缓存后都会调用 listenSession(),但 init() 这里没有。这样启动时恢复出来的会话后续触发 history.onMessageAdditionalChange 时不会自动持久化。

🔄 建议修复
       savedSessions.forEach((session) => {
         this.#sessionModels.set(session.sessionId, session);
+        this.listenSession(session);
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/chat/chat-manager.service.ts` around lines 160
- 164, Preloaded sessions created via fromJSON and inserted into the
`#sessionModels` map in init() are not registered with listenSession(), so later
history.onMessageAdditionalChange events won't trigger auto-save; update the
init() flow that loops over savedSessions (the block where
savedSessions.forEach(session => this.#sessionModels.set(session.sessionId,
session))) to call listenSession(session) (or the equivalent registration
method) after adding each session to `#sessionModels`, mirroring what
startSession() and getSession() do.
packages/ai-native/src/node/acp/acp-agent.service.ts-453-460 (1)

453-460: ⚠️ Potential issue | 🟠 Major

这里会对无 update 的通知直接解引用。

同文件的 loadSession() 已经把 notification.update 当成可空处理了,这里却直接访问 update.sessionUpdate。只要 agent 推来同 session 的非 update 通知,这个回调就会抛异常并中断当前流。

🛡️ 建议修复
   ): void {
     const update = notification.update;
+    if (!update) {
+      return;
+    }
 
     switch (update.sessionUpdate) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/node/acp/acp-agent.service.ts` around lines 453 - 460,
handleNotification dereferences notification.update without null-check which
throws when non-update notifications arrive; change handleNotification (handling
SessionNotification) to guard for a missing update at the top (e.g. if
(!notification.update) { /* ignore or handle non-update notification and return
*/ }) or use a safe-access pattern before reading update.sessionUpdate so the
stream isn't broken by non-update notifications; mirror the nullable handling
used in loadSession() and ensure any early return still properly maintains
stream/pendingToolCalls semantics.
packages/ai-native/src/browser/index.ts-119-123 (1)

119-123: ⚠️ Potential issue | 🟠 Major

AINativeBrowserContribution 被重复注册了。

前面已经注册过一次了。Contribution 类通常会注册命令、菜单和监听器,重复注入很容易带来重复 side effect。

🧹 建议修复
-    AINativeBrowserContribution,
     AcpPermissionDialogContribution,
     PermissionDialogManager,
     AcpPermissionBridgeService,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/index.ts` around lines 119 - 123,
AINativeBrowserContribution is registered twice in the exported contribution
list causing duplicate side effects; remove the duplicate entry so
AINativeBrowserContribution appears only once (leave other exports like
AcpPermissionDialogContribution, PermissionDialogManager,
AcpPermissionBridgeService intact) and verify any registration/DI container
usage only consumes the single AINativeBrowserContribution symbol to avoid
double command/menu/listener registration.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: dab2d7ed-03ed-4eeb-aec1-41abd2767dec

📥 Commits

Reviewing files that changed from the base of the PR and between 4ccd454 and 539a245.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (60)
  • packages/ai-native/__test__/browser/chat/chat-manager.service.test.ts
  • packages/ai-native/__test__/node/acp/cli-agent-process-manager.test.ts
  • packages/ai-native/package.json
  • packages/ai-native/src/browser/acp/acp-permission-rpc.service.ts
  • packages/ai-native/src/browser/acp/index.ts
  • packages/ai-native/src/browser/acp/permission-bridge.service.ts
  • packages/ai-native/src/browser/acp/permission-dialog-container.module.less
  • packages/ai-native/src/browser/acp/permission-dialog-container.tsx
  • packages/ai-native/src/browser/acp/permission-dialog.module.less
  • packages/ai-native/src/browser/acp/permission-dialog.view.tsx
  • packages/ai-native/src/browser/acp/permission.handler.ts
  • packages/ai-native/src/browser/ai-core.contribution.ts
  • packages/ai-native/src/browser/chat/acp-chat-agent.ts
  • packages/ai-native/src/browser/chat/acp-session-provider.ts
  • packages/ai-native/src/browser/chat/apply.service.ts
  • packages/ai-native/src/browser/chat/chat-agent.service.ts
  • packages/ai-native/src/browser/chat/chat-agent.view.service.ts
  • packages/ai-native/src/browser/chat/chat-edit-resource.ts
  • packages/ai-native/src/browser/chat/chat-manager.service.ts
  • packages/ai-native/src/browser/chat/chat-model.ts
  • packages/ai-native/src/browser/chat/chat-multi-diff-source.ts
  • packages/ai-native/src/browser/chat/chat-proxy.service.ts
  • packages/ai-native/src/browser/chat/chat.api.service.ts
  • packages/ai-native/src/browser/chat/chat.feature.registry.ts
  • packages/ai-native/src/browser/chat/chat.internal.service.ts
  • packages/ai-native/src/browser/chat/chat.render.registry.ts
  • packages/ai-native/src/browser/chat/chat.view.tsx
  • packages/ai-native/src/browser/chat/default-chat-agent.ts
  • packages/ai-native/src/browser/chat/local-storage-provider.ts
  • packages/ai-native/src/browser/chat/session-provider-registry.ts
  • packages/ai-native/src/browser/chat/session-provider.ts
  • packages/ai-native/src/browser/components/ChatHistory.tsx
  • packages/ai-native/src/browser/components/ChatMentionInput.tsx
  • packages/ai-native/src/browser/components/mention-input/mention-input.tsx
  • packages/ai-native/src/browser/components/mention-input/types.ts
  • packages/ai-native/src/browser/components/permission-dialog-widget.module.less
  • packages/ai-native/src/browser/components/permission-dialog-widget.tsx
  • packages/ai-native/src/browser/index.ts
  • packages/ai-native/src/browser/layout/ai-layout.tsx
  • packages/ai-native/src/common/acp-types.ts
  • packages/ai-native/src/common/agent-types.ts
  • packages/ai-native/src/common/index.ts
  • packages/ai-native/src/common/prompts/empty-prompt-provider.ts
  • packages/ai-native/src/node/acp/acp-agent.service.ts
  • packages/ai-native/src/node/acp/acp-cli-back.service.ts
  • packages/ai-native/src/node/acp/acp-cli-client.service.ts
  • packages/ai-native/src/node/acp/acp-permission-caller.service.ts
  • packages/ai-native/src/node/acp/cli-agent-process-manager.ts
  • packages/ai-native/src/node/acp/handlers/agent-request.handler.ts
  • packages/ai-native/src/node/acp/handlers/constants.ts
  • packages/ai-native/src/node/acp/handlers/file-system.handler.ts
  • packages/ai-native/src/node/acp/handlers/terminal.handler.ts
  • packages/ai-native/src/node/acp/index.ts
  • packages/ai-native/src/node/index.ts
  • packages/core-browser/src/ai-native/ai-config.service.ts
  • packages/core-common/src/log.ts
  • packages/core-common/src/storage.ts
  • packages/core-common/src/types/ai-native/index.ts
  • packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts
  • packages/startup/entry/web/server.ts

Comment on lines +108 to +127
listenReadable<IChatProgress>(stream, {
onData: (data) => {
progress(data);
},
onEnd: () => {
chatDeferred.resolve();
},
onError: (error) => {
this.messageService.error(error.message);
this.aiReporter.end(request.sessionId + '_' + request.requestId, {
message: error.message,
success: false,
command,
});
},
});

await chatDeferred.promise;
return {};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

错误时 chatDeferred 未被 resolve,导致 Promise 永远挂起

onError 回调中,chatDeferred 没有被 resolve 或 reject,这会导致 invoke 方法永远不会返回,造成调用方阻塞。

🐛 建议修复
     listenReadable<IChatProgress>(stream, {
       onData: (data) => {
         progress(data);
       },
       onEnd: () => {
         chatDeferred.resolve();
       },
       onError: (error) => {
         this.messageService.error(error.message);
         this.aiReporter.end(request.sessionId + '_' + request.requestId, {
           message: error.message,
           success: false,
           command,
         });
+        chatDeferred.resolve();
       },
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
listenReadable<IChatProgress>(stream, {
onData: (data) => {
progress(data);
},
onEnd: () => {
chatDeferred.resolve();
},
onError: (error) => {
this.messageService.error(error.message);
this.aiReporter.end(request.sessionId + '_' + request.requestId, {
message: error.message,
success: false,
command,
});
},
});
await chatDeferred.promise;
return {};
}
listenReadable<IChatProgress>(stream, {
onData: (data) => {
progress(data);
},
onEnd: () => {
chatDeferred.resolve();
},
onError: (error) => {
this.messageService.error(error.message);
this.aiReporter.end(request.sessionId + '_' + request.requestId, {
message: error.message,
success: false,
command,
});
chatDeferred.resolve();
},
});
await chatDeferred.promise;
return {};
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/browser/chat/default-chat-agent.ts` around lines 108 -
127, The onError handler passed to listenReadable does not settle chatDeferred,
leaving the invoke caller hanging; update the onError callback in
default-chat-agent.ts to both log/report the error (keep
this.messageService.error and aiReporter.end) and then settle chatDeferred
(either call chatDeferred.reject(error) to propagate the failure or
chatDeferred.resolve() if you want a graceful completion), ensuring the invoke
flow (the await chatDeferred.promise and return {}) always resumes; reference
the listenReadable(...) onError, the chatDeferred variable, and the
aiReporter.end(...) call when making the change.

Comment on lines +346 to +364
async createDirectory(request: FileSystemRequest): Promise<FileSystemResponse> {
const dirPath = this.resolvePath(request.path);
if (!dirPath) {
return {
error: {
code: ACPErrorCode.SERVER_ERROR,
message: 'Invalid path',
data: { path: request.path },
},
};
}

try {
const uri = URI.file(dirPath);
await this.fileService.createFolder(uri.toString());

this.logger?.log(`Directory created: ${dirPath}`);

return {};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

createDirectory() 绕过了写权限确认。

Line 346-364 这里直接修改工作区文件系统,没有复用 writeTextFile()permissionCallback 流程。如果 ACP 的权限模型要求所有写操作都要授权,这里就是一个直接的绕过口。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-native/src/node/acp/handlers/file-system.handler.ts` around lines
346 - 364, createDirectory(...) currently bypasses the ACP write-permission flow
by calling fileService.createFolder(...) directly; before creating the folder
(in the createDirectory method that uses resolvePath, URI.file and
fileService.createFolder), invoke the same permission check used by
writeTextFile (the permissionCallback/authorization flow) and only proceed to
call fileService.createFolder when the permission check grants write access;
ensure you reuse the existing permission-check helper or call the same
permissionCallback path, and preserve logging (this.logger.log) and error
response shape when permission is denied.

@lulusir
Copy link
Contributor Author

lulusir commented Mar 16, 2026

/next

@opensumi
Copy link
Contributor

opensumi bot commented Mar 16, 2026

🎉 PR Next publish successful!

3.9.1-next-1773654797.0

@lulusir
Copy link
Contributor Author

lulusir commented Mar 17, 2026

/next

@opensumi
Copy link
Contributor

opensumi bot commented Mar 17, 2026

🎉 PR Next publish successful!

3.9.1-next-1773716644.0

@lulusir
Copy link
Contributor Author

lulusir commented Mar 18, 2026

/next

@lulusir
Copy link
Contributor Author

lulusir commented Mar 18, 2026

/next

@opensumi
Copy link
Contributor

opensumi bot commented Mar 18, 2026

🎉 PR Next publish successful!

3.9.1-next-1773819640.0

@lulusir
Copy link
Contributor Author

lulusir commented Mar 18, 2026

/next

@opensumi
Copy link
Contributor

opensumi bot commented Mar 18, 2026

🎉 PR Next publish successful!

3.9.1-next-1773827963.0

@lulusir
Copy link
Contributor Author

lulusir commented Mar 19, 2026

/next

@lulusir
Copy link
Contributor Author

lulusir commented Mar 19, 2026

/next

@opensumi
Copy link
Contributor

opensumi bot commented Mar 19, 2026

🎉 PR Next publish successful!

3.9.1-next-1773914436.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant