Skip to content

feat(search): Obsidian-style global search with file/folder targeting#494

Open
KevinYoung-Kw wants to merge 18 commits intoop7418:mainfrom
KevinYoung-Kw:feat/global-search
Open

feat(search): Obsidian-style global search with file/folder targeting#494
KevinYoung-Kw wants to merge 18 commits intoop7418:mainfrom
KevinYoung-Kw:feat/global-search

Conversation

@KevinYoung-Kw
Copy link
Copy Markdown
Contributor

@KevinYoung-Kw KevinYoung-Kw commented Apr 15, 2026

Summary

在原有“仅搜索会话名”的基础上,升级为 Obsidian 风格的全局搜索:支持搜索会话、消息内容、文件、目录,并附带上下文预览和直达定位。

Scope 说明: message-level 的精确滚动定位(点击消息跳转到会话内部某条消息并高亮)因为和 StickToBottom 的自动回底存在 race condition,已经拆到单独的 feat/message-deep-link 分支,不在本 PR 内。本 PR 只包含稳定可用的搜索 UI + 文件/目录定位。


与原搜索的区别

维度 之前 现在
搜索范围 只能搜 会话标题 会话标题、消息内容文件名目录名
结果展示 平铺 10 条混合结果,看不出消息属于哪个会话 消息按 会话折叠分组;文件、会话各自独立成组
命中预览 显示消息 snippet,关键词高亮
文件定位 点击后自动打开文件树、展开目录、滚动到目标节点并闪烁高亮
global-search-demo

设计思路

1. 搜索结果分组展示

之前搜到 10 条消息时,根本不知道它们来自哪几个会话。现在消息按会话折叠分组,文件和会话也各自成组,扫一眼就能定位到想找的上下文。

2. 结果 preview 更友好

  • 对话框放宽到 sm:max-w-3xl,snippet 能显示更多内容
  • 固定 min(80vh, 520px) 高度,输入框始终置顶,不会因结果加载而上下跳动
  • 命中关键词用主题色高亮,一眼知道为什么匹配
  • 用图标区分 human / assistant / tool / file / folder

3. 文件/目录可搜索、可直达

  • 之前只能搜到文件,现在目录也可以被搜索(比如只记得 tests/ 但不记得具体文件名)
  • 点击后自动打开右侧文件树、展开父目录、滚动到目标节点并闪烁高亮
  • 考虑到文件树会在 AI 流式输出结束后自动刷新,加了 seekKeyRef 保护,防止刷新后把用户正在手动浏览的滚动位置又拽回高亮节点

4. 搜索前缀更顺手

提示文案改成单数 session: / message: / file:,API 同时兼容复数旧写法。snippet 截取策略把关键词往前靠,避免被单行 truncate 截掉。


Follow-up fixes (post review)

根据 UI/UX 验收反馈,补充了以下稳定性与可用性修复:

  1. 默认全局搜索补齐文件/目录维度

    • scope=all 现在也会返回 files(不再需要必须写 file: 才能命中)
  2. 文件 deep-link 不再被默认面板初始化覆盖

    • 从搜索结果进入 ?file= 时,优先保持文件树打开,避免“点了结果但文件树被关掉”
  3. 关闭搜索弹窗时中止 in-flight 请求

    • close/unmount 都会 abort 当前请求,避免弹窗重开后出现残留旧结果
  4. 关键词高亮对前缀查询生效

    • session:xxx / message:xxx / file:xxx 会用解析后的实际关键词 xxx 进行高亮
  5. 消息分组折叠头支持键盘操作

    • 用可聚焦的 CommandItem 承载折叠逻辑,支持 command 列表内键盘导航与回车切换
  6. IME 结束时避免重复请求

    • 去掉 onCompositionEnd 里的即时搜索,统一走去抖查询

Test plan

  • npm run test passes
  • 搜索关键词 → 消息按会话分组、图标正常
  • 默认搜索(不加前缀)可返回文件/目录结果
  • file:xxx → 仅显示文件/目录结果
  • 点击文件/目录 → 文件树自动展开并定位到目标
  • 树自动刷新后,手动滚动不再被抢回
  • 缩放窗口 → 对话框高度平滑变化,无跳动
  • 前缀查询下关键词高亮正确
  • 键盘可切换消息分组折叠

Relates to #482

KevinYoung-Kw and others added 14 commits April 15, 2026 13:18
**问题**
切换 Session 后,`StreamingMessage` 底部的计时器会从 0 重新开始计数。根因是 `ElapsedTimer` 组件在 mount 时用 `Date.now()` 本地初始化开始时间,Session 切换导致组件 remount 后时间重置。

**修复**
- `stream-session-manager` 的 `SessionStreamSnapshot` 已包含 `startedAt` 字段,记录流的真实开始时间
- 将 `startedAt` 从 `ChatView` → `MessageList` → `StreamingMessage` → `StreamingStatusBar` → `ElapsedTimer` 逐级透传
- `ElapsedTimer` 改为基于传入的 `startedAt` 计算 elapsed,组件 remount 后仍能恢复真实累计时长

**影响范围**
仅影响流式响应状态下的底部计时器显示,不改变任何持久化逻辑或计时行为。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tional details

**问题**
在输入框中已有文字时,选择带有 "Add details(optional)" 的斜杠命令(非 immediate badge 类命令),输入框内容会被直接清空。期望已有文本应作为 details 保留。

**修复**
- `src/lib/message-input-logic.ts`
  - `resolveItemSelection` 的 `set_badge` 分支现在计算 `newInputValue`,去掉 `/` 触发符和过滤文本,保留触发位置前后的用户输入内容
- `src/hooks/useSlashCommands.ts`
  - `insertItem` 的 `set_badge` 分支使用 `result.newInputValue` 回填输入框,替代之前的硬编码 `setInputValue('')`
- `electron/main.ts`
  - dev 模式端口支持 `PORT` 环境变量,避免与本地其他服务冲突

**影响范围**
仅影响 badge 类斜杠命令的输入框回填行为;immediate 命令(如 /clear)仍保持原有清空逻辑不变。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
**补充修复**
在 op7418#1 斜杠命令保留已有文本的修复基础上,进一步确保选择命令后光标自动定位到输入框尾部,而不是默认的首部。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add /api/search endpoint supporting scoped queries (sessions:, messages:, files:) and default cross-dimension search
- Add GlobalSearchDialog using cmdk CommandDialog with grouped results
- Add Cmd/Ctrl+K shortcut via useGlobalSearchShortcut hook
- Wire ChatListPanel search button to open global search
- Remove legacy session-only search dialog from ChatListPanel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Redesign the global search dialog with session-grouped messages,
Obsidian-style previews, and larger dialog sizing.

- GlobalSearchDialog: group messages by session with foldable groups;
  distinguish user/assistant/tool via icons; enlarge to sm:max-w-3xl;
  highlight matched keyword in snippet with primary color
- File/Folder search: pass ?file=path&q=query; auto-open file tree,
  expand parent folders and target directory, scroll and flash-highlight
  the matched item (files and directories both supported)
- Search API:兼容单数前缀 (session:/message:/file:) and return
  contentType for icon selection; folders are now searchable
- Snippet generation: bias keyword toward the front so it survives
  single-line truncation in the UI list
- i18n: add globalSearch.toolLabel for zh/en

Relates to op7418#482

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Give CommandDialog a fixed height (h-[60vh] max-h-[600px]) so the
overall dialog no longer expands and contracts as results appear.
Remove max-h from CommandList and let it fill remaining space with
flex-1, so only the result list scrolls while the input stays put.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…arning

- Use h-[min(80vh,520px)] for smooth viewport scaling instead of
  breakpoint-based hard switch
- Override CommandList default max-h-[300px] with max-h-none so
  results fill the entire dialog and the bottom white area is gone
- Replace <button> in CommandGroup heading with <div> to silence
  the aria-hidden/focus browser warning
- Remove unused FolderOpen import

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…dings

Apply bg-muted/40, rounded corners, and font-medium text-foreground
to session-level message group headers so they visually separate
from individual message items and create clearer hierarchy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Conflicts:
#	src/hooks/useSlashCommands.ts
…animations

Switch AIFileTree from defaultExpanded to controlled expanded so that
changing highlightPath actually opens parent folders in real time.
Add polling (100ms × 15) instead of a single setTimeout so the
scroll-to-highlight waits for Collapsible animation to finish.
Reset flash tracker on highlightPath change to avoid stale state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…reshes

Replace the global hasFlashedRef flag with a seekKeyRef tied to the
specific highlightPath. This stops the polling interval from restarting
whenever the file tree auto-refreshes (e.g. after streaming ends), which
was causing users to be snapped back to the highlighted file while they
were manually scrolling.

Also removes the unnecessary loading dependency from the scroll effect.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 15, 2026

@KevinYoung-Kw is attempting to deploy a commit to the op7418's projects Team on Vercel.

A member of the Team first needs to authorize it.

- add seek token on file result navigation

- use reactive search params in chat/file-tree panels

- key file-tree seeking by path+seek token

- degrade update API to no-update payload on upstream failures
- avoid consuming seek key before target is found

- include workingDirectory in seek key

- clear stale tree state on project switch

- add Playwright regression for repeated + cross-session file seeks
- cover all/session/message/file search modes

- seed sessions/messages/files deterministically

- make Cmd/Ctrl+K open global search even while editing
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