Skip to content
Merged
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
13 changes: 6 additions & 7 deletions docs/architecture/tool-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -624,16 +624,15 @@ class AgentFileSystemHandler {

### YoBrowser CDP 工具

YoBrowser 提供基于 Chrome DevTools Protocol (CDP) 的最小工具集,在 agent 模式下直接可用
YoBrowser 在 agent 模式下直接提供 session 级单实例 browser 能力,每个 session 最多绑定一个 sidepanel browser

**可用工具**:
- `yo_browser_tab_list` - 列出所有浏览器 tabs
- `yo_browser_tab_new` - 创建新 tab
- `yo_browser_tab_activate` - 激活指定 tab
- `yo_browser_tab_close` - 关闭 tab
- `yo_browser_cdp_send` - 发送 CDP 命令
- `load_url` - 懒创建当前 session 的 browser 并导航到目标 URL
- `get_browser_status` - 返回当前 session browser 的页面、导航和可见性状态
- `cdp_send` - 向当前 session browser 发送 CDP 命令

**安全约束**:
**约束**:
- `cdp_send` 不会自动创建 browser;必须先调用 `load_url`
- `local://` URL 禁止 CDP attach(在 `BrowserTab.ensureSession()` 中检查)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Update the stale implementation reference for the local:// CDP guard.

The mention of BrowserTab.ensureSession() looks outdated for the new session-scoped architecture; point this line to the current guard location used in the presenter/tool handler path.

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

In `@docs/architecture/tool-system.md` at line 636, Update the docs line that
currently references BrowserTab.ensureSession(): replace that stale
implementation pointer with a reference to the session-scoped CDP guard used in
the presenter/tool handler path (i.e., point readers to the presenter/tool
handler CDP guard instead of BrowserTab.ensureSession()). Locate the old mention
of BrowserTab.ensureSession() in the sentence and change it to explicitly
reference the presenter/tool-handler session guard (the guard implemented in the
presenter/tool handler path) so the doc points to the current code location.

- 所有 CDP 命令通过 `webContents.debugger.sendCommand()` 执行

Expand Down
42 changes: 42 additions & 0 deletions docs/specs/yobrowser-optimization/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# YoBrowser Session 单实例实施计划

## 1. 主进程模型

1. `YoBrowserPresenter` 用 `Map<sessionId, SessionBrowserState>` 替代全局单状态。
2. 每个 `SessionBrowserState` 仅包含一个 `WebContentsView`、一个 `BrowserTab`、attach 信息、可见性和最后一次 bounds。
3. `load_url` 负责首次懒加载:创建 browser、发起 sidepanel open、等待 host ready、再导航。

## 2. 工具路由

1. `YoBrowserToolDefinitions` 仅注册 `load_url`、`get_browser_status`、`cdp_send`。
2. `YoBrowserToolHandler.callTool` 必须接收 `conversationId` 并据此路由 session。
3. `AgentToolManager` 和 `ToolPresenter` 把这 3 个名字视为内建 YoBrowser 工具。
4. MCP 同名工具在定义收集阶段直接过滤。

## 3. Renderer 行为

1. `BrowserPanel` 接收 `sessionId`,所有 presenter 调用都显式带 sessionId。
2. 切换 session 时先 detach 旧 session browser。
3. 若旧 session 状态不是 `working`,立即 destroy。
4. 若旧 session 状态是 `working`,加入 `pendingBrowserDestroySessionIds`,等状态变更后再 destroy。
5. YoBrowser 事件 payload 带 `sessionId`,只更新当前 panel 对应的会话。

## 4. 独立 browser 下线

1. 删除 `src/renderer/browser` 旧壳入口。
2. `windowPresenter` 中旧 `browser` window 类型不再创建独立窗口,统一回退到 chat window。
3. 清理 `tabPresenter` 中依赖 `browserTabId` 的 YoBrowser 分支。

## 5. 测试策略

1. main:
- tool definitions 只剩 3 个新工具。
- 旧工具名报 unknown tool。
- `load_url` 懒加载与 host-ready 流程成立。
- session 间 browser state 隔离。
2. renderer:
- `BrowserPanel` 仅响应当前 session 事件。
- session 切换时的 detach / destroy / pending destroy 成立。
3. 回归:
- `cdp_send` 仍走 offload。
- disabled tools 存储和展示使用新工具名。
82 changes: 42 additions & 40 deletions docs/specs/yobrowser-optimization/spec.md
Original file line number Diff line number Diff line change
@@ -1,64 +1,66 @@
# YoBrowser Optimization(UI + CDP 工具)
# YoBrowser Session 单实例收敛

## 背景

当前 YoBrowser 在 Workspace 侧边栏存在 UI 问题:
- `src/renderer/src/components/workspace/WorkspaceView.vue` 在 `agent` 模式下总会渲染 `WorkspaceBrowserTabs` 分区,即便没有任何 tab,也会出现一块空区域。
当前 YoBrowser 还保留了多 window / 多 tab 的旧抽象,但实际运行时已经收敛为单个 sidepanel browser host。继续暴露 `open / close / focus / list`、windowId、tabId 等接口,只会增加状态分叉和错误恢复成本。

## 目标(Goals)
同时,session 切换后 browser 的回收策略需要和会话状态对齐:如果旧 session 仍在 `working`,切走时只能先 detach,不能立刻销毁,否则会打断本轮工具调用。

1. **UI**:只有存在 YoBrowser tabs 时,Workspace 侧边栏才显示 Browser Tabs 分区。
2. **Agent 工具直接注入**:YoBrowser 工具(`yo_browser_*`)在 agent 模式下直接可用,无需激活任何 skill。
## 目标

## 非目标(Non-Goals)
1. 每个 session 最多持有一个 YoBrowser `webContents`。
2. agent 仅暴露 3 个裸工具名:`load_url`、`get_browser_status`、`cdp_send`。
3. `load_url` 首次调用时懒创建 browser,并自动完成 sidepanel attach 流程。
4. session 切换时按会话状态销毁:
- 非 `working`:立即 detach 并销毁。
- `working`:先 detach,待状态结束后再销毁。
5. 下线旧独立 browser shell,只保留聊天右侧 sidepanel 的 YoBrowser。

- 不调整 YoBrowser window 的 UI、尺寸、布局、位置策略。
- 不修改 `BrowserContextBuilder.buildSystemPrompt` 的注入策略(不做减少/压缩/裁剪)。
- 不改造其他 agent 工具(filesystem/bash/mcp 等)。
- 不使用 skills 系统来控制 YoBrowser 工具的可见性。
## 非目标

## 用户故事(User Stories)
- 不扩成通用多窗口 browser 系统。
- 不保留旧 `yo_browser_*` 别名兼容。
- 不让 `cdp_send` 自动创建 browser;必须先 `load_url`。
- 不额外重构通用 window presenter 架构。

- 作为用户,我不希望在没有任何浏览器 tab 的情况下,Workspace 侧边栏仍出现空的 Browser Tabs 分区。
- 作为 agent 用户,我希望 YoBrowser 自动化能力以 CDP 为核心,工具在 agent 模式下直接可用。
## 用户故事

## 约束与假设(Constraints & Assumptions)
- 作为 agent 用户,我希望 browser 工具是直接、稳定、少状态的,不需要理解 window/tab 多实体模型。
- 作为使用多会话的用户,我希望切换 session 时前一个 session 的 browser 不串到当前会话。
- 作为正在执行 browser 工具的用户,我希望切走会话不会打断仍在运行中的 browser 操作。

- YoBrowser 现有实现已经基于 Electron Debugger/CDP(`CDPManager`, `BrowserTab.ensureSession()`)。
- 安全边界:`local://` URL 禁止绑定 CDP(`BrowserTab` 现有逻辑已做限制)。
## 约束与假设

## 验收标准(Acceptance Criteria)
- “正在 loading” 统一按当前 session 状态 `working` 处理。
- 一个 session 同时只允许一个 sidepanel browser 实例。
- `cdp_send` 永远绑定当前 tool call 的 `conversationId`。
- `load_url`、`get_browser_status`、`cdp_send` 视为内建保留工具名,MCP 不得覆盖。

### A. UI:Workspace Browser Tabs 展示逻辑
## 验收标准

- [ ] `src/renderer/src/components/workspace/WorkspaceView.vue` 仅在 `chatMode === 'agent' && yoBrowserStore.tabCount > 0` 时渲染 `WorkspaceBrowserTabs`。
- [ ] 当 `tabCount === 0` 时,不显示 Browser Tabs 分区(不保留空白区域)。
### A. 工具面

### B. 工具:YoBrowser CDP 工具直接注入(agent 模式)
- [ ] agent tool definitions 仅包含 `load_url`、`get_browser_status`、`cdp_send`。
- [ ] 旧 `yo_browser_*` 名称调用时返回 unknown tool。
- [ ] `cdp_send` 若 session browser 尚未初始化,返回明确错误,要求先 `load_url`。

- [ ] agent tool definitions 中包含 `yo_browser_*` 工具(agent 模式下直接可用)。
- [ ] agent 的 tool call 路由正确处理 `yo_browser_*` 工具(`toolName.startsWith('yo_browser_')`)。
- [ ] 不依赖 skills 系统(不检查 `activeSkills`)。
### B. Session 生命周期

### C. 工具实现:CDP 方式 + 合适的参数定义
- [ ] `load_url` 首次调用时才创建对应 session 的 browser。
- [ ] 不同 session 持有各自独立 browser state,不共享 page / visibility / attach 状态。
- [ ] session 切换时,旧 session 若非 `working`,立即 destroy。
- [ ] session 切换时,旧 session 若为 `working`,仅 detach;该 session 结束后再 destroy。

- [ ] 工具集合:
- `yo_browser_tab_list`:列出 tabs 与 active tab。
- `yo_browser_tab_new`:创建新 tab(可选 url)。
- `yo_browser_tab_activate`:激活 tab。
- `yo_browser_tab_close`:关闭 tab。
- `yo_browser_cdp_send`:向指定/当前 tab 的 CDP session 发送 `{ method, params }`。
- [ ] 参数 schema 符合 CDP 使用方式(method、params 等)。
- [ ] 保留安全边界:`local://` 禁止 CDP attach。
### C. UI 与事件

### D. Prompt/Context
- [ ] Renderer 仅响应当前 `sessionId` 的 YoBrowser 事件。
- [ ] 切换 session 后,browser panel 不显示前一个 session 的状态。
- [ ] 旧独立 browser shell 入口不再可用。

- [ ] `BrowserContextBuilder.buildSystemPrompt` 的注入保持现状(不做减少/压缩/裁剪)。
### D. 文档与接口

### E. 兼容性

- [ ] 不涉及数据迁移。
- [ ] 现有 YoBrowser UI/窗口/Tab 生命周期保持可用。
- [ ] `IYoBrowserPresenter` 与共享类型收敛到 session-aware 单实例接口。
- [ ] 架构文档与本 spec 使用新工具名与新生命周期语义。

## Open Questions

Expand Down
9 changes: 9 additions & 0 deletions docs/specs/yobrowser-optimization/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# YoBrowser Session 单实例任务拆分

1. 收敛共享类型与 presenter 接口到 session-aware 单实例模型。
2. 重写 `YoBrowserPresenter` 的 session 状态管理、attach、detach、destroy 流程。
3. 将 tool definitions / handler / agent routing 切到 `load_url`、`get_browser_status`、`cdp_send`。
4. 在 renderer sidepanel 中按 `sessionId` 驱动 browser panel,并实现 `working` 态延迟销毁。
5. 删除旧独立 browser shell 与 `browserTabId` 相关残留。
6. 更新 main / renderer / agent presenter 测试到新工具名和新生命周期。
7. 更新规格与架构文档,并跑格式化、i18n、lint、关键测试。
2 changes: 0 additions & 2 deletions electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ export default defineConfig({
resolve: {
alias: {
'@': resolve('src/renderer/src'),
'@browser': resolve('src/renderer/browser'),
'@shared': resolve('src/shared'),
"@shadcn": resolve('src/shadcn'),
vue: 'vue/dist/vue.esm-bundler.js'
Expand Down Expand Up @@ -107,7 +106,6 @@ export default defineConfig({
cssCodeSplit: false,
rollupOptions: {
input: {
browser: resolve('src/renderer/browser/index.html'),
index: resolve('src/renderer/index.html'),
floating: resolve('src/renderer/floating/index.html'),
splash: resolve('src/renderer/splash/index.html'),
Expand Down
24 changes: 23 additions & 1 deletion src/main/lib/agentRuntime/sessionPaths.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { createHash } from 'crypto'
import { app } from 'electron'
import path from 'path'

const INVALID_WINDOWS_SEGMENT_CHARS = new Set(['<', '>', ':', '"', '/', '\\', '|', '?', '*'])
const TRAILING_WINDOWS_SEGMENT_CHARS = /[. ]+$/g

export function getSessionsRoot(): string {
return path.resolve(app.getPath('home'), '.deepchat', 'sessions')
}
Expand Down Expand Up @@ -29,7 +33,7 @@ export function resolveToolOffloadPath(conversationId: string, toolCallId: strin
return null
}

const safeToolCallId = toolCallId.replace(/[\\/]/g, '_')
const safeToolCallId = sanitizeToolCallIdForOffload(toolCallId)
return path.join(sessionDir, `tool_${safeToolCallId}.offload`)
}

Expand All @@ -41,3 +45,21 @@ export function resolveToolOffloadTemplatePath(conversationId: string): string |

return path.join(sessionDir, 'tool_<toolCallId>.offload')
}

function sanitizeToolCallIdForOffload(toolCallId: string): string {
const sanitizedBase = Array.from(toolCallId.trim(), (char) => {
const charCode = char.charCodeAt(0)
if (charCode <= 0x1f || INVALID_WINDOWS_SEGMENT_CHARS.has(char)) {
return '_'
}

return char
})
.join('')
.replace(TRAILING_WINDOWS_SEGMENT_CHARS, '')

const fingerprint = createHash('sha1').update(toolCallId).digest('hex').slice(0, 8)
const sanitized = [sanitizedBase || 'tool_call', fingerprint].filter(Boolean).join('_')

return sanitized || 'tool_call'
}
6 changes: 4 additions & 2 deletions src/main/presenter/agentPresenter/acp/agentToolManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
CHAT_SETTINGS_TOOL_NAMES
} from './chatSettingsTools'
import type { AgentToolRuntimePort } from '../runtimePorts'
import { YO_BROWSER_TOOL_NAMES } from '../../browser/YoBrowserToolDefinitions'

// Consider moving to a shared handlers location in future refactoring
import {
Expand Down Expand Up @@ -66,6 +67,7 @@ interface AgentToolManagerOptions {
}

export class AgentToolManager {
private static readonly YO_BROWSER_TOOL_NAME_SET = new Set<string>(YO_BROWSER_TOOL_NAMES)
private agentWorkspacePath: string | null
private fileSystemHandler: AgentFileSystemHandler | null = null
private bashHandler: AgentBashHandler | null = null
Expand Down Expand Up @@ -393,8 +395,8 @@ export class AgentToolManager {
}

// Route to YoBrowser CDP tools
if (toolName.startsWith('yo_browser_')) {
const response = await this.getYoBrowserToolHandler().callTool(toolName, args)
if (AgentToolManager.YO_BROWSER_TOOL_NAME_SET.has(toolName)) {
const response = await this.getYoBrowserToolHandler().callTool(toolName, args, conversationId)
return {
content: response
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const QUESTION_ERROR_KEY = 'common.error.invalidQuestionRequest'

// Tools that require offload when output exceeds threshold
// Tools not in this list will never trigger offload (e.g., read has its own pagination)
const TOOLS_REQUIRING_OFFLOAD = new Set(['exec', 'ls', 'find', 'grep', 'yo_browser_cdp_send'])
const TOOLS_REQUIRING_OFFLOAD = new Set(['exec', 'ls', 'find', 'grep', 'cdp_send'])

export class ToolCallProcessor {
constructor(private readonly options: ToolCallProcessorOptions) {}
Expand Down
20 changes: 6 additions & 14 deletions src/main/presenter/browser/BrowserContextBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
import type { BrowserToolDefinition, BrowserWindowInfo } from '@shared/types/browser'
import type { BrowserToolDefinition, YoBrowserStatus } from '@shared/types/browser'

export class BrowserContextBuilder {
static buildSystemPrompt(windows: BrowserWindowInfo[], activeWindowId: number | null): string {
const activeWindow = windows.find((browserWindow) => browserWindow.id === activeWindowId)
const windowLines =
windows.length === 0
? ['- No browser windows open.']
: windows.map((browserWindow) => {
const marker = browserWindow.id === activeWindowId ? '*' : ' '
const title = browserWindow.page.title || browserWindow.page.url || 'Untitled'
return `${marker} ${title} (${browserWindow.page.url || 'about:blank'})`
})
static buildSystemPrompt(status: YoBrowserStatus): string {
const page = status.page
const pageLine = page ? `${page.title || page.url || 'Untitled'} (${page.url})` : 'none'

return [
'Yo Browser is available for web exploration.',
`Active window: ${activeWindow ? `${activeWindow.page.title || activeWindow.page.url} (${activeWindow.id})` : 'none'}`,
'Open browser windows:',
...windowLines,
`Current page: ${pageLine}`,
'Use Yo Browser to browse, extract DOM, run scripts, capture screenshots, and download files.'
].join('\n')
}
Expand Down
Loading
Loading