Skip to content

fix(web): debounce and cancel stale file searches - #1702

Open
techotaku39 wants to merge 2 commits into
tiann:mainfrom
techotaku39:fix/web-file-search-cancel-debounce
Open

fix(web): debounce and cancel stale file searches#1702
techotaku39 wants to merge 2 commits into
tiann:mainfrom
techotaku39:fix/web-file-search-cancel-debounce

Conversation

@techotaku39

@techotaku39 techotaku39 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Debounce Web file-search queries by 300 ms before starting a new search.
  • Propagate AbortSignal from the browser request through Hub RPC to the CLI.
  • Cancel superseded searches and terminate their in-flight rg process.
  • Abort active RPC handlers when the CLI socket disconnects.
  • Bound file metadata collection to batches of 16 and stop between batches when a request is canceled.
  • Preserve existing result limits, wildcard matching, sorting, and file metadata behavior.

Problem / Motivation

Previously, every file-search query change immediately started the full Web → Hub → Socket.IO → CLI → rg pipeline. 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

  • Low-to-medium risk: the change spans the existing Web → Hub → CLI RPC path.
  • Request IDs are optional, so existing non-cancellable RPC calls retain their current behavior.
  • File metadata stat() calls are limited to 16 concurrent operations per batch.
  • No database migration or new dependency is required.
  • The change can be rolled back by reverting the commits in this PR.

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 (from cli/) — 17 tests passed.
  • bun test src/sync/rpcGateway.test.ts src/web/routes/git.test.ts (from hub/) — 19 tests passed.
  • bun run test -- src/hooks/queries/useSessionFileSearch.test.tsx src/api/client.test.ts (from web/) — 18 tests passed.
  • git diff --check — passed.
  • GitHub Actions test — passed, including bun run test:e2e -- terminal-wrap-fidelity.spec.ts composer-copy.spec.ts.
  • GitHub Actions drift-gate — passed.
  • GitHub Actions 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).

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Findings

  • [Major] 元数据阶段并未真正取消 CLI 工作 — 路由把请求信号传给 statFiles,Gateway 也会发送 rpc-cancel,但 CLI 的 StatFiles handler 仍只接收 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,相关 catch cli/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

Comment thread hub/src/web/routes/git.ts
.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))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[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'))
}

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 test and drift-gate passed.
  • GitHub integration failed on the same four runner assertions as base commit bc9df82d; not attributable to this diff.

HAPI Bot

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