Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"i18n-ally.namespace": true,
"i18n-ally.pathMatcher": "{locale}/{namespaces}.json",
"unocss.disable": true,
"typescript.experimental.useTsgo": true
"js/ts.experimental.useTsgo": true
}
300 changes: 300 additions & 0 deletions docs/specs/app-spotlight-search/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
# App Spotlight Search 实施计划

## 1. 当前实现基线

### 1.1 已有搜索能力

1. 当前 PR 已在 `ChatPage.vue` 中实现会话内 inline 搜索,支持:
- `Cmd/Ctrl+F`
- 当前消息正文高亮
- Enter / Shift+Enter 导航
- Esc 关闭
2. 消息正文 DOM 已统一暴露 `data-message-content="true"`,可作为会话内搜索与消息跳转高亮的稳定边界。
3. 左侧 `WindowSideBar.vue` 已支持会话标题过滤与局部高亮,但范围仅限当前会话列表。

### 1.2 快捷键现状

1. `src/main/presenter/configPresenter/shortcutKeySettings.ts` 已维护 renderer/system 级默认快捷键。
2. `ShortcutPresenter` 负责主进程注册与重注册快捷键。
3. `SHORTCUT_EVENTS` 已经承载 main -> renderer 的快捷键分发。

### 1.3 设置导航现状

1. 设置窗口独立于主聊天窗口。
2. `src/renderer/settings/main.ts` 已维护设置页路由,并在 `meta` 中提供 `titleKey`、`icon`、`position`。
3. `SETTINGS_EVENTS.NAVIGATE` 已支持主进程打开/聚焦设置窗口后导航到某个设置页。

### 1.4 历史数据现状

1. 会话和消息已经持久化在 SQLite 相关链路中。
2. 当前没有统一给 UI 使用的“历史全文搜索”主进程服务。
3. 当前 PR 的会话内搜索主要基于 renderer DOM,而非历史索引。

## 2. 设计决策

### 2.1 功能边界

Spotlight 作为 follow-up feature 分两层:

1. **主进程历史搜索**
- 只负责会话 / 历史消息检索
- 输出结构化命中结果
2. **渲染层统一混排**
- 将历史搜索结果与 `agent / setting / action` 本地条目合并
- 统一排序、裁剪、active item 管理与执行行为

这样可以避免把 Agent / Setting / Action 这些天然本地导航项也塞进数据库索引。

### 2.2 默认快捷键

最终采用:

- `QuickSearch = CommandOrControl+P`

原因:

1. 当前会话内搜索已经使用 `Cmd/Ctrl+F`
2. Spotlight 是“全局跳转器”,语义更接近 `Cmd/Ctrl+P`
3. 避免同一个快捷键在 inline search 与 Spotlight 之间抢焦点

### 2.3 搜索索引表

新增两张表:

1. `deepchat_search_documents`
- `entity_kind`
- `entity_id`
- `session_id`
- `title`
- `body`
- `role`
- `updated_at`
2. `deepchat_search_documents_fts`
- 对 `title/body` 做 FTS5 全文索引

用途分离:

1. 普通表作为真实数据表与降级查询源
2. FTS 表只负责全文索引与匹配性能

### 2.4 索引数据来源

#### 会话文档

- `entity_kind = 'session'`
- `entity_id = session.id`
- `session_id = session.id`
- `title = session.title`
- `body = ''`

#### 消息文档

- `entity_kind = 'message'`
- `entity_id = message.id`
- `session_id = message.sessionId`
- `title = session.title`(冗余存储,便于结果展示和排序)
- `body = 可见文本`
- `role = user | assistant`

### 2.5 可见文本抽取规则

需要抽离一个可复用的“消息可见文本抽取”模块,供:

1. Spotlight 历史搜索
2. 未来 MCP history search
3. 可能的导出 / snippet 生成复用

抽取规则:

1. 用户消息:
- 优先取结构化 `content[].type === 'text'`
- 回退到 `text`
2. 助手消息:
- 只收集 `content` 类型可见文本
- 收集 `error` block 文本
- 可选收集 `plan` 可见文本
3. 忽略:
- tool call 原始 JSON
- image/audio 元数据
- search result raw payload
- streaming 中但未落库内容

### 2.6 查询与降级

查询顺序:

1. 优先 FTS5
2. 若 FTS 执行失败,或 query 被 tokenizer 拒绝,则回退到 `LIKE`

降级目标:

1. 任何输入都尽量返回可用结果
2. 避免特殊字符 query 直接“零结果 + 无解释”

### 2.7 结果类型与共享接口

新增共享类型:

1. `HistorySearchOptions`
2. `HistorySearchSessionHit`
3. `HistorySearchMessageHit`
4. `HistorySearchHit`

新增 Presenter 接口:

- `INewAgentPresenter.searchHistory(query, options?)`

说明:

1. Presenter 只返回 `session | message` 命中
2. `agent | setting | action` 在 renderer 本地组装为 `SpotlightItem`

### 2.8 设置导航 registry

为避免把设置搜索逻辑散落在 Spotlight 组件中,新增一个共享 registry:

- `routeName`
- `titleKey`
- `icon`
- `keywords[]`

数据来源优先复用 `src/renderer/settings/main.ts` 路由元信息,必要时抽成共享常量,让:

1. 设置窗口侧栏
2. Spotlight setting items

消费同一份元数据,避免标题 / icon 漂移。

### 2.9 Renderer 状态与执行流

新增 `spotlight store`,最少维护:

1. `open`
2. `query`
3. `results`
4. `activeIndex`
5. `loading`
6. `requestSeq`
7. `pendingMessageJump`

执行流程:

1. 打开面板 -> 聚焦输入框
2. 输入 80ms debounce
3. 发起历史搜索请求
4. 本地合并 `agent / setting / action`
5. renderer 做统一排序和截断
6. Enter / click 执行目标

### 2.10 消息跳转串联

`message` 结果执行后:

1. 先写入 `pendingMessageJump = { sessionId, messageId }`
2. 切换会话
3. Chat 页在消息加载完成后消费 `pendingMessageJump`
4. 找到目标消息 DOM
5. 滚动到目标
6. 高亮约 2 秒
7. 清空 `pendingMessageJump`

这样可以避免在会话尚未激活、消息尚未加载完成时提前滚动失败。

## 3. 事件流

### 3.1 快捷键打开

1. `ShortcutPresenter` 注册 `QuickSearch`
2. 触发时发 `SHORTCUT_EVENTS.TOGGLE_SPOTLIGHT`
3. 主聊天 renderer 的 `App.vue` 或 Spotlight host 监听该事件
4. 切换 Spotlight open 状态

### 3.2 设置命中跳转

1. renderer 执行 setting item
2. 调用主进程打开/聚焦设置窗口
3. 发送 `SETTINGS_EVENTS.NAVIGATE`
4. 设置窗口跳到对应页面

### 3.3 Message 命中跳转

1. renderer 执行 message item
2. 写入 `pendingMessageJump`
3. 调用现有 session 选择逻辑
4. `ChatPage` 在消息加载完成后消费跳转

## 4. 测试策略

### 4.1 Main

1. session title hit 与 message hit 的排序基础正确
2. 会话重命名后索引更新
3. 消息编辑 / 删除后索引更新
4. legacy import / schema rebuild 后全量回填可用
5. FTS 查询失败时自动降级到 `LIKE`

### 4.2 Renderer

1. `Cmd/Ctrl+P` 打开 / `Esc` 关闭
2. 输入框自动聚焦
3. `↑/↓/Home/End/Enter` 全链路
4. 混排列表稳定,不因 hover 破坏 active item
5. `message` 结果切会话后成功滚动并高亮
6. `agent / setting / action` 执行正确
7. 空查询 recent/agents/actions 可见

### 4.3 验收场景

1. sidebar 收起时 Spotlight 仍可用
2. 有查询时最多显示 12 条结果
3. 结果 hover 与 click 行为自然
4. 设置窗口已有焦点时,从主聊天窗口触发 Spotlight 仍只打开主聊天窗口里的唯一实例

## 5. 风险与缓解

### 风险 1:FTS 索引同步遗漏

如果消息编辑 / 删除 / 导入路径漏掉同步,搜索结果会漂移。

缓解:

1. 把索引同步集中到统一服务
2. 对 create/update/delete/import 分别补 main tests

### 风险 2:Spotlight 与 inline chat search 快捷键冲突

缓解:

1. Spotlight 默认改为 `Cmd/Ctrl+P`
2. 保留 inline chat search 的 `Cmd/Ctrl+F`
3. 在快捷键设置页显式展示两者

### 风险 3:设置页 metadata 分散

缓解:

1. 抽共享 registry
2. 让设置窗口与 Spotlight 共同消费

### 风险 4:消息跳转时序不稳定

缓解:

1. 用 `pendingMessageJump` 记录目标
2. 只在会话激活 + 消息加载完成后消费

### 风险 5:大库搜索性能抖动

缓解:

1. 历史查询只请求 `session | message`
2. renderer 最多渲染 12 条
3. 使用 debounce + requestSeq 丢弃过期结果

## 6. 质量门槛

1. `pnpm run format`
2. `pnpm run i18n`
3. `pnpm run lint`
4. `pnpm run typecheck`
5. 相关 main / renderer 测试通过
Loading
Loading