fix(web): debounce and cancel stale file searches - #1702
Conversation
There was a problem hiding this comment.
Findings
- [Major] 元数据阶段并未真正取消 CLI 工作 — 路由把请求信号传给
statFiles,Gateway 也会发送rpc-cancel,但 CLI 的StatFileshandler 仍只接收data,并立即通过Promise.all启动全部路径的stat()。浏览器取消后,Hub 会停止等待,但 CLI 仍会完成默认 200、最多 500 个文件元数据请求;连续旧搜索仍可积累工作。证据hub/src/web/routes/git.ts:266,相关处理器cli/src/modules/common/handlers/directories.ts:100。
Suggested fix:rpcHandlerManager.registerHandler<StatFilesRequest, StatFilesResponse>( RPC_METHODS.StatFiles, async (data, signal) => { const entries = [] for (let offset = 0; offset < data.paths.length; offset += 16) { signal?.throwIfAborted() entries.push(...await Promise.all( data.paths.slice(offset, offset + 16).map(statFile) )) } return { success: true, entries } } )
- [Minor] 预期的搜索取消仍被记录成 ripgrep 失败 —
runFileSearch新增的取消路径会抛出AbortError,但外层 handler 会先捕获、记录并转换成普通 RPC error,因此RpcHandlerManager的免日志分支永远看不到真实文件搜索取消;新增测试只覆盖了直接抛错的合成 handler。证据cli/src/modules/common/handlers/ripgrep.ts:35,相关 catchcli/src/modules/common/handlers/ripgrep.ts:43。
Suggested fix:} catch (error) { if (error instanceof Error && error.name === 'AbortError') throw error logger.debug('Failed to run ripgrep:', error) return rpcError(getErrorMessage(error, 'Failed to run ripgrep')) }
Summary
Review mode: initial
- 发现 1 个 Major、1 个 Minor;取消链路在元数据阶段不完整,实际文件搜索取消仍产生误导性失败日志。
Testing
- Not run (automation;安全约束禁止执行 PR 代码)。
- GitHub
test已通过;integration失败于 4 个 runner integration assertions,目前未证明与本 diff 有关。 - 建议补充:通过已注册的 ripgrep handler 取消运行中搜索并断言无失败日志;在
statFiles处理中途取消并断言后续批次未启动。
HAPI Bot
| .slice(0, limit) | ||
|
|
||
| const metadataResult = await runRpc(() => engine.statFiles(sessionResult.sessionId, paths)) | ||
| const metadataResult = await runRpc(() => engine.statFiles(sessionResult.sessionId, paths, c.req.raw.signal)) |
There was a problem hiding this comment.
[MAJOR] 元数据 RPC 的取消信号在 CLI 端无人消费
这里开始把请求信号传给 statFiles,Gateway 会生成 requestId 并发送 rpc-cancel;但实际 handler cli/src/modules/common/handlers/directories.ts:100 仍是 async (data),并用一个 Promise.all 立即启动全部 stat()。因此浏览器取消只让 Hub 停止等待,CLI 仍会跑完默认 200、最多 500 个元数据操作,旧搜索工作仍可累积。
Suggested fix:
rpcHandlerManager.registerHandler<StatFilesRequest, StatFilesResponse>(
RPC_METHODS.StatFiles,
async (data, signal) => {
const entries = []
for (let offset = 0; offset < data.paths.length; offset += 16) {
signal?.throwIfAborted()
entries.push(...await Promise.all(
data.paths.slice(offset, offset + 16).map(statFile)
))
}
return { success: true, entries }
}
)将现有单路径 stat 逻辑提取为 statFile,按小批次执行并在批次间检查取消。
| try { | ||
| const result = data.fileSearch | ||
| ? await runFileSearch(data.args, { ...data.fileSearch, cwd: data.cwd }) | ||
| ? await runFileSearch(data.args, { ...data.fileSearch, cwd: data.cwd }, signal) |
There was a problem hiding this comment.
[MINOR] AbortError 在到达免日志分支前被转换了
runFileSearch(..., signal) 取消时会抛出 AbortError,但本 handler 的现有 catch 会先执行 logger.debug('Failed to run ripgrep:', error) 并返回普通 rpcError。因此 RpcHandlerManager 新增的 isAbortError 分支无法抑制真实文件搜索取消日志;当前测试只覆盖直接抛出 AbortError 的合成 handler。
Suggested fix:
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') throw error
logger.debug('Failed to run ripgrep:', error)
return rpcError(getErrorMessage(error, 'Failed to run ripgrep'))
}There was a problem hiding this comment.
Findings
- No findings.
Summary
Review mode: follow-up after new commits
- No correctness, security, regression, data-loss, performance, or maintainability issue met the reporting threshold in the latest full diff.
- The prior Major and Minor findings no longer reproduce: metadata work now stops between bounded stat batches, and expected ripgrep aborts pass through without failure logging.
- Residual testing gap: cancellation is covered at the Web hook, Hub gateway/route, CLI RPC manager, ripgrep wrapper, and metadata-handler layers, but there is no true cross-process Web → Hub → CLI cancellation test.
Testing
- Not run (automation; review security policy prohibits executing PR code).
- GitHub
testanddrift-gatepassed. - GitHub
integrationfailed on the same four runner assertions as base commitbc9df82d; not attributable to this diff.
HAPI Bot
Summary
AbortSignalfrom the browser request through Hub RPC to the CLI.rgprocess.Problem / Motivation
Previously, every file-search query change immediately started the full Web → Hub → Socket.IO → CLI →
rgpipeline. Rapid search changes could leave stale searches running in parallel, consuming CLI and Hub resources even though their results could no longer be displayed.This change makes each search request disposable: a newer query cancels the older request, and the 300 ms debounce avoids starting work for intermediate query values. File metadata requests now use bounded batches and honor the same cancellation signal, so superseded searches do not continue starting metadata work after cancellation.
This PR covers file-search request scheduling and cancellation only. URL-controlled input synchronization and native keyboard/IME handling are intentionally out of scope.
User Impact
Users searching large workspaces should see fewer stale searches competing for resources, especially when rapidly changing the search query.
Search results continue to respect the existing result limit, wildcard semantics, sorting, and file metadata behavior.
Risk / Rollback
stat()calls are limited to 16 concurrent operations per batch.Validation
bun typecheck— passed.pwsh -NoProfile -File .\scripts\Invoke-HapiTaskPlaywright.ps1 -Name file-search-cancel-debounce -Suite Root -TestArgs terminal-wrap-fidelity.spec.ts— 2 tests passed.bun run build— passed.bun run test -- src/api/rpc/RpcHandlerManager.test.ts src/modules/common/handlers/directories.test.ts src/modules/common/handlers/ripgrep.test.ts src/modules/ripgrep/index.test.ts(fromcli/) — 17 tests passed.bun test src/sync/rpcGateway.test.ts src/web/routes/git.test.ts(fromhub/) — 19 tests passed.bun run test -- src/hooks/queries/useSessionFileSearch.test.tsx src/api/client.test.ts(fromweb/) — 18 tests passed.git diff --check— passed.test— passed, includingbun run test:e2e -- terminal-wrap-fidelity.spec.ts composer-copy.spec.ts.drift-gate— passed.pr-review— passed; follow-up review reported no findings.Related Issues
Refs #1699 (file-search request scheduling and cancellation only)
AI Disclosure
Implemented and validated with assistance from OpenAI Codex (GPT-5.6).