Skip to content

Universal Search Palette#709

Merged
arul28 merged 14 commits into
mainfrom
ade/universal-search-palette-527927bf
Jul 6, 2026
Merged

Universal Search Palette#709
arul28 merged 14 commits into
mainfrom
ade/universal-search-palette-527927bf

Conversation

@arul28

@arul28 arul28 commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Universal search + global command palette

Makes everything in ADE searchable — lanes, chats including full message content, CLI sessions including terminal scrollback, PRs, commits, branches, files, Linear issues, and proof artifacts — through one deterministic search service, surfaced in the desktop ⌘K palette, an ade search CLI command for agents, and the ade code TUI palette.

Search service (apps/desktop/src/main/services/search/)

  • FTS5 index (porter/unicode61) in a separate disposable SQLite cache at .ade/cache/search-index.db — never inside ade.db (FTS virtual tables can't be cr-sqlite CRRs, the index must not sync, and a rebuild is just a file drop). Schema-versioned: mismatch or corruption drops and rebuilds.
  • Indexed kinds: chat (per-message docs with deep links to the event), terminal (ANSI-stripped, newline-aligned scrollback chunks with byte-offset deep links), PRs (title/body/comments), commits (recent 100/lane), branches (primary lane only). Delegated at query time for freshness: lanes, files (existing file index), artifacts (local SQLite), Linear (opt-in only — network path never runs by default).
  • Ingestion: debounced background queue with per-source byte cursors tailing the transcript files; live events (chat commit, PTY data, PR refresh, session/lane changes) only mark sources dirty — nothing blocks the PTY/chat hot paths. Bounded, resumable backfill starts well after boot.
  • Deterministic ranking, exactly specified: exact-title > title-prefix > title-substring > BM25 body, ties by updatedAt desc then docId; message/chunk docs rank body-only so a chat's title doesn't promote every message. A title-scoped candidate union keeps the ranking spec true even when body matches saturate the candidate window.
  • Query syntax: bare terms AND, "quoted phrases", kind: / lane: / session: / since: filters.

search action domain

search.query / search.indexStatus (all roles) and search.rebuildIndex (CTO-only), registered through the daemon action registry and constructed in both desktop main and the ade serve daemon via a shared wiring factory — no preload/in-process bypass (runtime-backed null-services bug class). Caller scoping mirrors the chat/terminal read policy: session-bound agents see only their own session's chat/terminal content; unbound agent roles get no session content; user CLI/desktop/CTO keep whole-project search.

Surfaces

  • Desktop ⌘K: the existing palette gains typed entity sections (Chats/Terminals/PRs/Lanes/Commits/Branches/Files/Issues/Artifacts) below commands — debounced, generation-guarded, highlighted matches, lane chips, relative times, per-section show-more, Enter navigates per kind. Command mode and the project browser are untouched.
  • CLI: ade search "<query>" [--kind a,b] [--lane <id|name>] [--limit N] [--cursor c] [--json|--text], plus --status/--rebuild; exit 1 on no results. New ade-search agent skill + AGENTS.md line + bundled skill advertisement.
  • TUI: Ctrl+K palette merges server-ranked chat/terminal hits below local matches with jump-to-session.

Verification

  • 49 service tests (ingestion cursors, deletion, ranking determinism, scoping, chunking, parser) + 4 RPC-gate scoping tests + registry/CLI/palette suites; live-daemon e2e against the real project (6k+ docs; warm FTS query <1ms vs the 50 ms palette budget).
  • Known limitation (documented): desktop + daemon both ingest when both are up — convergent and corruption-free, at the cost of duplicate IO.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added ade search across CLI, desktop command palette, and TUI with coverage for chats, terminals, lanes, PRs, commits/branches, files/artifacts, and Linear.
    • Supports --kind/--lane, --limit/--cursor pagination, --status/--rebuild, and --text/--json output with ranked snippets and keyboard navigation (including “show more”).
  • Bug Fixes
    • Enforced correct caller scoping for search results and tightened permissions for indexing operations.
  • Documentation
    • Expanded CLI help text and added ade-search skill guidance.
  • Tests
    • Added end-to-end and unit coverage for query parsing, ranking, indexing, scoping, and exit-code behavior.

Greptile Summary

This PR adds a universal search layer across ADE — chat transcripts, terminal scrollback, PRs, commits, branches, lanes, files, Linear issues, and proof artifacts — surfaced through a desktop ⌘K palette, an ade search CLI command, and the TUI Ctrl+K palette. The search backend is a schema-versioned FTS5 SQLite cache with a debounced ingestion queue, deterministic ranking, and caller-scoped result filtering.

  • Search service (searchService.ts, searchIndexDb.ts, searchQueryParser.ts, searchRanking.ts, terminalChunking.ts): FTS5 index in a disposable .ade/cache/search-index.db; five FTS kinds (chat, terminal, PR, commit, branch) with ANSI-stripped chunking and cursor-resumable ingestion; lanes, files, artifacts, and Linear delegated at query time; callerScope enforcement mirrors the existing chat/terminal read scoping.
  • Surfaces: Desktop ⌘K palette gains typed entity sections below commands with generation-guarded debounced backend queries; ade search CLI adds --kind/--lane/--limit/--cursor flags and script-friendly exit codes; TUI palette merges server-ranked hits with local matches.
  • Action wiring: search.query / search.indexStatus (all roles) and search.rebuildIndex (CTO-only) registered through the shared action registry; scopeSearchAdeActionArgs limits session-bound agents to their own session's content.

Confidence Score: 4/5

Safe to merge with one fix: the Show N more count in the palette renders the backend pre-limit total rather than the count of rows actually fetched, so clicking the button can promise hundreds of additional results while only revealing a handful.

The ingestion queue, ranking, scoping, FTS schema, and CLI are well-constructed. One concrete display bug in CommandPalette.tsx makes Show N more advertise a count drawn from totalByKind rather than the fetched page size, which misleads users when a kind has many total matches but few rows in the 60-result response window.

apps/desktop/src/renderer/components/app/CommandPalette.tsx — the hiddenCount computation needs the same Math.min cap that commandPaletteSearch.tsx already applies in flatEntities.

Important Files Changed

Filename Overview
apps/desktop/src/renderer/components/app/CommandPalette.tsx Adds universal entity sections below command rows. The flat-index interleaving and keyboard navigation are consistent. One confirmed bug: hiddenCount uses section.total (backend pre-limit count) instead of capping by section.rows.length, causing the "Show N more" button to advertise far more results than are actually available.
apps/desktop/src/main/services/search/searchService.ts Core 1441-line search service: FTS5 ingestion queue, debounced per-source processors, paginated query with delegated candidates, and scoped results. Ingestion logic looks solid. Invalid inline filters correctly return empty results. File candidates correctly receive the laneId parameter.
apps/desktop/src/main/services/search/searchIndexDb.ts Schema-versioned FTS5 SQLite cache with WAL/NORMAL sync settings. Drop-and-recreate on version mismatch is well-implemented.
apps/desktop/src/main/services/search/searchQueryParser.ts Deterministic tokenizer with quoted-phrase support, filter prefixes, and FTS5 MATCH expression builder. Double-quote escaping is correct; parseSince guards against out-of-range Date values.
apps/desktop/src/renderer/components/app/commandPaletteSearch.tsx New file providing useUniversalSearch hook, SearchResultRow, ShowMoreRow, and highlight helpers. Generation-guarded debounce is correct; flatEntities correctly caps hiddenCount by fetched rows.
apps/desktop/src/main/services/search/searchServiceWiring.ts Shared wiring factory for desktop and daemon. Session/chat event subscriptions, deferred backfill, and dispose chain look correct.
apps/ade-cli/src/cli.ts Adds ade search command with --kind, --lane, --limit, --cursor, --status, --rebuild flags. Exit-code-from-result pattern is clean.
apps/ade-cli/src/adeRpcServer.ts Adds scopeSearchAdeActionArgs scoping session-bound callers to their own session's content. Unbound callers intentionally keep whole-project search.
apps/desktop/src/main/services/search/searchRanking.ts Deterministic four-tier ranking with updatedAt/docId tie-breaks. Snippet marker extraction is clean.
apps/desktop/src/main/services/search/terminalChunking.ts Newline-aligned ANSI-stripped chunking with UTF-8 safe cuts and infinite-loop guard.
apps/ade-cli/src/tuiClient/app.tsx TUI palette correctly re-reads connectionRef.current inside the debounce timer. Session-based deduplication against local items is correct.

Reviews (5): Last reviewed commit: "search: round-5 review fixes — serialize..." | Re-trigger Greptile

arul28 and others added 8 commits July 6, 2026 09:18
…ic ranking, terminal chunking

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…p main + ade serve daemon + preload bridge

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… enqueue

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…palette

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sor-chat classification, unlimited backfill enumeration, primary-lane branch dedup)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…se guard, torn-tail safety, title-tier candidate union, shared host wiring factory, palette search module extraction

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eads, totals consistency on title union

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tests, docs feature page, CLI help/README parity

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview Jul 6, 2026 6:39pm

@arul28

arul28 commented Jul 6, 2026

Copy link
Copy Markdown
Owner Author

@copilot review but do not make fixes

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@arul28, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a344a5b0-6e68-40d3-9699-db31d9c397df

📥 Commits

Reviewing files that changed from the base of the PR and between 78b445c and 2236887.

📒 Files selected for processing (2)
  • apps/desktop/src/main/services/search/searchService.ts
  • apps/desktop/src/shared/types/search.ts
📝 Walkthrough

Walkthrough

This PR adds universal search across desktop and ADE CLI, including shared search contracts, indexing/query logic, runtime wiring, CLI/TUI integration, desktop command-palette support, and docs.

Changes

Universal search feature

Layer / File(s) Summary
Shared contracts and search primitives
apps/desktop/src/shared/types/search.ts, apps/desktop/src/shared/types/index.ts, apps/desktop/src/main/services/search/searchQueryParser.ts, apps/desktop/src/main/services/search/searchRanking.ts, apps/desktop/src/main/services/search/terminalChunking.ts, apps/desktop/src/main/services/search/searchIndexDb.ts, searchQueryParser.test.ts
Defines shared search types, query parsing, ranking, terminal chunking, and SQLite index helpers, with parser/chunking tests.
Search service indexing and query
apps/desktop/src/main/services/search/searchService.ts, searchService.test.ts
Implements ingestion, delegated querying, ranking, pagination, status/rebuild, and service validation tests.
Runtime wiring and search exposure
apps/desktop/src/main/services/search/searchServiceWiring.ts, apps/desktop/src/main/main.ts, apps/ade-cli/src/bootstrap.ts, apps/desktop/src/main/services/adeActions/registry.ts, registry.test.ts, apps/ade-cli/src/adeRpcServer.ts, adeRpcServer.test.ts, apps/desktop/src/main/services/ipc/registerIpc.ts, apps/desktop/src/preload/global.d.ts, preload.ts
Creates and exposes the search service, forwards lifecycle events, registers the search action domain, and exposes search through preload and IPC APIs.
ADE CLI search command and docs
apps/ade-cli/src/cli.ts, cli.test.ts, README.md, apps/desktop/resources/ade-cli-help.txt, apps/desktop/resources/agent-skills/ade-search/SKILL.md, apps/desktop/scripts/regen-ade-cli-help.cjs, apps/desktop/src/shared/adeCliGuidance.ts
Adds the ade search command, formatter/output handling, help text, skill docs, guidance updates, and help regeneration.
TUI command palette search
apps/ade-cli/src/tuiClient/app.tsx
Adds debounced universal search results and search-result navigation to the TUI palette.
Desktop command palette search UI
apps/desktop/src/renderer/components/app/commandPaletteSearch.tsx, CommandPalette.tsx
Adds universal entity search rendering, highlighting, selection, activation, and merged palette navigation in the desktop UI.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested labels: desktop

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and clearly matches the PR’s main change: adding a universal search palette.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ade/universal-search-palette-527927bf

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@copilot review but do not make fixes

Review complete (no fixes applied).

I found one meaningful issue:

  • Medium: since: duration parsing can throw for very large finite values. In apps/desktop/src/main/services/search/searchQueryParser.ts (parseSince), new Date(now.getTime() - amount * unitMs).toISOString() can hit an invalid date range and throw RangeError, which bubbles out of query handling.

Repro example: since:100000000000d.

If you want, I can follow up with a surgical fix in a separate pass.

Copilot finished work on behalf of arul28 July 6, 2026 14:41
Comment thread apps/desktop/src/main/services/search/searchService.ts Outdated
…palette-527927bf

# Conflicts:
#	docs/ARCHITECTURE.md
Comment thread apps/desktop/src/main/services/search/searchService.ts

@coderabbitai coderabbitai 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.

Actionable comments posted: 6

🧹 Nitpick comments (6)
apps/desktop/src/main/services/search/searchService.ts (2)

1225-1300: 🧹 Nitpick | 🔵 Trivial

Offset-based pagination is not isolated from concurrent background re-indexing.

query() recomputes the full ranked candidate list on every call and pages via a plain offset. Since ingestion runs on a debounced background queue that can insert/update docs between two page fetches for the same UI search session, a user paging through results while chat/terminal activity is being indexed could see duplicate or skipped items across pages. This is a reasonable trade-off for a local, single-user index, but worth being aware of if "next page" correctness during active sessions becomes a support issue — a keyset-based cursor (e.g. (tier, bm25/updatedAt, docId)) would be more resilient but is a larger change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/search/searchService.ts` around lines 1225 -
1300, The pagination in query() uses a plain offset cursor over a freshly
re-ranked result set, so concurrent re-indexing can cause duplicates or skipped
items between pages. If exact next-page stability matters for searchService.ts,
switch the cursor logic in decodeCursor/encodeCursor and the query() paging flow
to a keyset-based cursor using stable sort keys from rankCandidates (for example
tier, score or updatedAt, and docId) instead of offset.

377-397: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use run().lastInsertRowid instead of a follow-up SELECT.

Node's StatementSync.run() already returns { changes, lastInsertRowid }, so the extra SELECT id FROM docs WHERE doc_id = ? after the INSERT is an avoidable round-trip on a hot ingestion path (backfill of large chat/terminal histories calls this many times).

⚡ Proposed refactor
-    db.prepare(
+    const inserted = db.prepare(
       `INSERT INTO docs (doc_id, kind, lane_id, lane_name, session_id, title, rank_title, snippet_source, deep_link, updated_at)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
     ).run(
       doc.docId,
       doc.kind,
       doc.laneId,
       doc.laneName,
       doc.sessionId,
       doc.title,
       doc.rankTitle,
       doc.snippetSource,
       doc.deepLink,
       doc.updatedAt
     );
-    const inserted = db.prepare("SELECT id FROM docs WHERE doc_id = ?").get(doc.docId) as { id: number };
     db.prepare("INSERT INTO docs_fts (rowid, rank_title, body) VALUES (?, ?, ?)").run(
-      inserted.id,
+      inserted.lastInsertRowid,
       doc.rankTitle ?? "",
       clampBody(doc.body)
     );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/search/searchService.ts` around lines 377 -
397, The docs insertion path in searchService should avoid the extra lookup
after inserting into docs. Update the code that performs the INSERT and then
queries by docId so it uses the result from StatementSync.run() and reads
lastInsertRowid directly, then pass that rowid into the docs_fts insert. Keep
the change localized around the docs insert/fts insert flow in searchService to
preserve the existing behavior while removing the follow-up SELECT.
apps/desktop/src/main/services/search/searchService.test.ts (1)

52-65: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the duplicated transcript-writing test helpers.

writeChatLine/writeTerminalOutput are re-declared identically across three describe blocks in this file. A shared test-utils module would reduce duplication as more search-service test suites are added.

Also applies to: 321-328

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/search/searchService.test.ts` around lines 52
- 65, The transcript-writing helpers are duplicated across multiple search
service test blocks, so extract `writeChatLine` and `writeTerminalOutput` into a
shared test utility and reuse them from each `describe` in
`searchService.test.ts`. Keep the helper behavior the same, but centralize the
filesystem setup and append logic so future search-service tests can import the
same helpers instead of redeclaring them.
apps/desktop/src/main/services/search/searchIndexDb.ts (1)

66-74: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Recreate path can stamp a mismatched schema as "current" if file removal silently fails.

If tryRemoveDbFiles can't actually delete the old db file (e.g. locked on Windows), create() re-runs CREATE TABLE/INDEX IF NOT EXISTS against the same, still-mismatched file and then unconditionally writes the new schemaVersion into meta — leaving stale/incompatible table structure marked as healthy. Consider having create() explicitly DROP TABLE/INDEX IF EXISTS before recreating, rather than relying solely on file deletion succeeding.

♻️ Proposed defense-in-depth fix
   const create = (): DatabaseSyncType => {
     const db = openAt(dbPath);
+    db.exec(`
+      DROP TABLE IF EXISTS docs_fts;
+      DROP TABLE IF EXISTS docs;
+      DROP TABLE IF EXISTS sources;
+      DROP TABLE IF EXISTS meta;
+    `);
     db.exec(DDL);
     db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('schemaVersion', ?)").run(
       String(SEARCH_INDEX_SCHEMA_VERSION)
     );
     return db;
   };

Also applies to: 106-134

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/search/searchIndexDb.ts` around lines 66 - 74,
The recreate flow can leave an old SQLite file in place and then mark it current
with a new schemaVersion, so update create() to actively reset the schema
instead of relying on tryRemoveDbFiles succeeding. In the create() path around
the existing CREATE TABLE/INDEX setup, first DROP TABLE/INDEX IF EXISTS for the
affected objects, then recreate them and only after that write the new value
into meta. Use tryRemoveDbFiles only as a best-effort cleanup, not as the only
safeguard against a mismatched schema.
apps/ade-cli/src/cli.ts (1)

5976-5983: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

--rebuild always exits 0, even when the rebuild didn't start.

Unlike the query path (which sets exitCodeFromResult to fail on empty results), the --rebuild branch has no exitCodeFromResult, so a SearchRebuildResult of { started: false } (e.g. non-CTO caller, or already running) still produces a 0 exit code. Given the feature's "script-friendly" exit-code pitch, callers doing ade search --rebuild && ... can't detect a rejected/no-op rebuild.

♻️ Suggested fix
   if (readFlag(args, ["--rebuild", "--rebuild-index"])) {
     return {
       kind: "execute",
       label: "search rebuild",
       formatter: "search-status",
       steps: [actionStep("result", "search", "rebuildIndex", {})],
+      exitCodeFromResult: (result) =>
+        isRecord(result) && result.started === false ? 1 : 0,
     };
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ade-cli/src/cli.ts` around lines 5976 - 5983, The --rebuild branch in
cli.ts always returns a successful execute action even when rebuildIndex does
not start, so add an exitCodeFromResult mapping for the search rebuild path in
the same place that builds the action object. Use the SearchRebuildResult from
actionStep("result", "search", "rebuildIndex", {}) and make it return a non-zero
exit code when started is false, matching the script-friendly behavior already
used in the query path.
apps/desktop/src/renderer/components/app/CommandPalette.tsx (1)

2221-2270: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Entity "visible/show-more" slicing is computed twice (here and in useUniversalSearch's flatEntities).

This render block recomputes expanded/visible/showMore/hiddenCount from entitySections independently of the identical computation already done in flatEntities (commandPaletteSearch.tsx). They currently agree, but activateFlat/keyboard selection rely on flatEntities's ordering matching this render's flat index exactly — any future edit to one copy without the other would silently desync selection from what's displayed. Consider having useUniversalSearch return pre-sliced, render-ready rows (e.g. add visibleRows/showMore/hiddenCount onto EntitySection) so both the index count and the render consume one source of truth.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/components/app/CommandPalette.tsx` around lines
2221 - 2270, The entity section rendering in CommandPalette is duplicating the
visible/show-more slicing logic already computed in
useUniversalSearch/flatEntities, which can cause the displayed rows and flat
selection indices to drift apart. Update the search data model so EntitySection
(or the useUniversalSearch result) carries pre-sliced, render-ready fields like
visibleRows, showMore, and hiddenCount, and make this render loop consume those
values directly. Keep activateFlat, flatEntities, and
SearchResultRow/ShowMoreRow ordering sourced from the same single source of
truth.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/ade-cli/src/cli.ts`:
- Around line 5960-5967: `buildSearchPlan` already handles the `--actions` flag,
but `HELP_BY_COMMAND.search` does not advertise it. Update the search help text
to mention `--actions`, or align the search command with the existing
service-command pattern by switching to the positional `actions --text`
convention; make sure the documented usage matches the behavior in
`buildSearchPlan` and related search command helpers.

In `@apps/desktop/src/main/main.ts`:
- Around line 4643-4647: Move the search service teardown in the shutdown flow
so it happens after the event-producing disposals; in the main cleanup function
in main.ts, keep ctx.ptyService?.disposeAll(), ctx.processService?.disposeAll(),
and ctx.agentChatService?.disposeAll() ahead of ctx.searchService?.dispose(),
and ensure searchServiceHolder is cleared as part of that final cleanup so no
later events can reach an already-disposed search service.

In `@apps/desktop/src/main/services/search/searchIndexDb.ts`:
- Around line 103-147: The openSearchIndexDb initialization path always executes
the FTS5 DDL, so on runtimes where node:sqlite lacks SQLITE_ENABLE_FTS5 the
create/open retry loop in openSearchIndexDb and create will fail repeatedly.
Update the search index setup to detect FTS5 support before running DDL, and
skip or disable search-index creation when the capability is unavailable;
alternatively switch to a SQLite build that includes FTS5. Keep the fallback
logic in openSearchIndexDb from retrying the same unsupported CREATE VIRTUAL
TABLE path.

In `@apps/desktop/src/main/services/search/searchServiceWiring.ts`:
- Around line 142-162: The `dispose()` implementation in
`createProjectSearchService` is leaving the `sessionService.onChanged` and
`agentChatService.subscribeToEvents` listeners attached, so each project switch
leaks handlers that keep calling a disposed search service. Capture the
unsubscribe handles returned by those two registrations in
`searchServiceWiring.ts`, store them alongside the `backfillTimer`, and invoke
them inside the returned `dispose()` before calling `searchService.dispose()`.

In `@apps/desktop/src/renderer/components/app/CommandPalette.tsx`:
- Around line 1122-1138: Guard the chat/terminal branch in CommandPalette so it
does not navigate when item.sessionId is missing. In the switch case handling
"chat" and "terminal", check sessionId before dispatching
"ade:work:select-session" and calling navigate; if it is falsy, skip both
actions or return early so /work?sessionId= is never created. Use the existing
sessionId, item.laneId, and navigate logic in this branch to place the guard
cleanly.

In `@apps/desktop/src/renderer/components/app/commandPaletteSearch.tsx`:
- Around line 361-378: The “Show N more” label in the command palette section
preview is using the backend’s total count instead of the number of rows
actually available in `section.rows`, so the displayed hidden count can
overpromise. Update the `flatEntities` memo in `commandPaletteSearch.tsx` to
derive `hiddenCount` from the fetched/visible data for each `section.kind` and
clamp it to what is actually present in `section.rows` (and the preview limit),
so expanding a section only advertises rows that can really be revealed without
another fetch.

---

Nitpick comments:
In `@apps/ade-cli/src/cli.ts`:
- Around line 5976-5983: The --rebuild branch in cli.ts always returns a
successful execute action even when rebuildIndex does not start, so add an
exitCodeFromResult mapping for the search rebuild path in the same place that
builds the action object. Use the SearchRebuildResult from actionStep("result",
"search", "rebuildIndex", {}) and make it return a non-zero exit code when
started is false, matching the script-friendly behavior already used in the
query path.

In `@apps/desktop/src/main/services/search/searchIndexDb.ts`:
- Around line 66-74: The recreate flow can leave an old SQLite file in place and
then mark it current with a new schemaVersion, so update create() to actively
reset the schema instead of relying on tryRemoveDbFiles succeeding. In the
create() path around the existing CREATE TABLE/INDEX setup, first DROP
TABLE/INDEX IF EXISTS for the affected objects, then recreate them and only
after that write the new value into meta. Use tryRemoveDbFiles only as a
best-effort cleanup, not as the only safeguard against a mismatched schema.

In `@apps/desktop/src/main/services/search/searchService.test.ts`:
- Around line 52-65: The transcript-writing helpers are duplicated across
multiple search service test blocks, so extract `writeChatLine` and
`writeTerminalOutput` into a shared test utility and reuse them from each
`describe` in `searchService.test.ts`. Keep the helper behavior the same, but
centralize the filesystem setup and append logic so future search-service tests
can import the same helpers instead of redeclaring them.

In `@apps/desktop/src/main/services/search/searchService.ts`:
- Around line 1225-1300: The pagination in query() uses a plain offset cursor
over a freshly re-ranked result set, so concurrent re-indexing can cause
duplicates or skipped items between pages. If exact next-page stability matters
for searchService.ts, switch the cursor logic in decodeCursor/encodeCursor and
the query() paging flow to a keyset-based cursor using stable sort keys from
rankCandidates (for example tier, score or updatedAt, and docId) instead of
offset.
- Around line 377-397: The docs insertion path in searchService should avoid the
extra lookup after inserting into docs. Update the code that performs the INSERT
and then queries by docId so it uses the result from StatementSync.run() and
reads lastInsertRowid directly, then pass that rowid into the docs_fts insert.
Keep the change localized around the docs insert/fts insert flow in
searchService to preserve the existing behavior while removing the follow-up
SELECT.

In `@apps/desktop/src/renderer/components/app/CommandPalette.tsx`:
- Around line 2221-2270: The entity section rendering in CommandPalette is
duplicating the visible/show-more slicing logic already computed in
useUniversalSearch/flatEntities, which can cause the displayed rows and flat
selection indices to drift apart. Update the search data model so EntitySection
(or the useUniversalSearch result) carries pre-sliced, render-ready fields like
visibleRows, showMore, and hiddenCount, and make this render loop consume those
values directly. Keep activateFlat, flatEntities, and
SearchResultRow/ShowMoreRow ordering sourced from the same single source of
truth.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b3e9da64-e212-4f2d-be47-4a61506365f3

📥 Commits

Reviewing files that changed from the base of the PR and between 04badb8 and 8e76ad9.

⛔ Files ignored due to path filters (7)
  • AGENTS.md is excluded by !*.md
  • docs/ARCHITECTURE.md is excluded by !docs/**
  • docs/PRD.md is excluded by !docs/**
  • docs/README.md is excluded by !docs/**
  • docs/features/chat/README.md is excluded by !docs/**
  • docs/features/search/README.md is excluded by !docs/**
  • docs/features/terminals-and-sessions/README.md is excluded by !docs/**
📒 Files selected for processing (29)
  • apps/ade-cli/README.md
  • apps/ade-cli/src/adeRpcServer.test.ts
  • apps/ade-cli/src/adeRpcServer.ts
  • apps/ade-cli/src/bootstrap.ts
  • apps/ade-cli/src/cli.test.ts
  • apps/ade-cli/src/cli.ts
  • apps/ade-cli/src/tuiClient/app.tsx
  • apps/desktop/resources/ade-cli-help.txt
  • apps/desktop/resources/agent-skills/ade-search/SKILL.md
  • apps/desktop/scripts/regen-ade-cli-help.cjs
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/adeActions/registry.test.ts
  • apps/desktop/src/main/services/adeActions/registry.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/main/services/search/searchIndexDb.ts
  • apps/desktop/src/main/services/search/searchQueryParser.test.ts
  • apps/desktop/src/main/services/search/searchQueryParser.ts
  • apps/desktop/src/main/services/search/searchRanking.ts
  • apps/desktop/src/main/services/search/searchService.test.ts
  • apps/desktop/src/main/services/search/searchService.ts
  • apps/desktop/src/main/services/search/searchServiceWiring.ts
  • apps/desktop/src/main/services/search/terminalChunking.ts
  • apps/desktop/src/preload/global.d.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/components/app/CommandPalette.tsx
  • apps/desktop/src/renderer/components/app/commandPaletteSearch.tsx
  • apps/desktop/src/shared/adeCliGuidance.ts
  • apps/desktop/src/shared/types/index.ts
  • apps/desktop/src/shared/types/search.ts

Comment thread apps/ade-cli/src/cli.ts
Comment thread apps/desktop/src/main/main.ts Outdated
Comment thread apps/desktop/src/main/services/search/searchIndexDb.ts
Comment on lines +142 to +162
sessionService.onChanged((event) => {
searchService.notifySessionChanged(
event.sessionId,
event.reason === "deleted" ? "deleted" : "meta-updated"
);
});
args.agentChatService?.subscribeToEvents?.((envelope) => {
searchService.notifyChatEvent(envelope.sessionId);
});

const backfillTimer = setTimeout(() => searchService.startBackfill(), args.backfillDelayMs);
backfillTimer.unref?.();

return {
...searchService,
dispose: () => {
clearTimeout(backfillTimer);
searchService.dispose();
}
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Listener leak: dispose() never unsubscribes session/chat event hooks.

sessionService.onChanged and agentChatService.subscribeToEvents are typed to return unknown (implying an unsubscribe handle) but the return values are discarded. dispose() only clears the backfill timer and calls searchService.dispose() — the outer subscriptions stay wired. Since createProjectSearchService runs again on every project switch (not just app restart), each switch leaves the previous listeners attached, invoking methods on an already-disposed search service on subsequent session/chat events.

🔧 Proposed fix
-  sessionService.onChanged((event) => {
-    searchService.notifySessionChanged(
-      event.sessionId,
-      event.reason === "deleted" ? "deleted" : "meta-updated"
-    );
-  });
-  args.agentChatService?.subscribeToEvents?.((envelope) => {
-    searchService.notifyChatEvent(envelope.sessionId);
-  });
+  const unsubscribeSessionChanged = sessionService.onChanged((event) => {
+    searchService.notifySessionChanged(
+      event.sessionId,
+      event.reason === "deleted" ? "deleted" : "meta-updated"
+    );
+  });
+  const unsubscribeChatEvents = args.agentChatService?.subscribeToEvents?.((envelope) => {
+    searchService.notifyChatEvent(envelope.sessionId);
+  });

   const backfillTimer = setTimeout(() => searchService.startBackfill(), args.backfillDelayMs);
   backfillTimer.unref?.();

   return {
     ...searchService,
     dispose: () => {
       clearTimeout(backfillTimer);
+      if (typeof unsubscribeSessionChanged === "function") unsubscribeSessionChanged();
+      if (typeof unsubscribeChatEvents === "function") unsubscribeChatEvents();
       searchService.dispose();
     }
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
sessionService.onChanged((event) => {
searchService.notifySessionChanged(
event.sessionId,
event.reason === "deleted" ? "deleted" : "meta-updated"
);
});
args.agentChatService?.subscribeToEvents?.((envelope) => {
searchService.notifyChatEvent(envelope.sessionId);
});
const backfillTimer = setTimeout(() => searchService.startBackfill(), args.backfillDelayMs);
backfillTimer.unref?.();
return {
...searchService,
dispose: () => {
clearTimeout(backfillTimer);
searchService.dispose();
}
};
}
const unsubscribeSessionChanged = sessionService.onChanged((event) => {
searchService.notifySessionChanged(
event.sessionId,
event.reason === "deleted" ? "deleted" : "meta-updated"
);
});
const unsubscribeChatEvents = args.agentChatService?.subscribeToEvents?.((envelope) => {
searchService.notifyChatEvent(envelope.sessionId);
});
const backfillTimer = setTimeout(() => searchService.startBackfill(), args.backfillDelayMs);
backfillTimer.unref?.();
return {
...searchService,
dispose: () => {
clearTimeout(backfillTimer);
if (typeof unsubscribeSessionChanged === "function") unsubscribeSessionChanged();
if (typeof unsubscribeChatEvents === "function") unsubscribeChatEvents();
searchService.dispose();
}
};
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/services/search/searchServiceWiring.ts` around lines
142 - 162, The `dispose()` implementation in `createProjectSearchService` is
leaving the `sessionService.onChanged` and `agentChatService.subscribeToEvents`
listeners attached, so each project switch leaks handlers that keep calling a
disposed search service. Capture the unsubscribe handles returned by those two
registrations in `searchServiceWiring.ts`, store them alongside the
`backfillTimer`, and invoke them inside the returned `dispose()` before calling
`searchService.dispose()`.

Comment on lines +1122 to +1138
case "chat":
case "terminal": {
const sessionId = item.sessionId ?? "";
// The Work tab stays mounted (keep-alive), so its select-session
// listener focuses the target; the navigate switches the visible tab.
window.dispatchEvent(
new CustomEvent("ade:work:select-session", {
detail: { sessionId, laneId: item.laneId ?? undefined },
}),
);
navigate(
`/work?sessionId=${encodeURIComponent(sessionId)}${
item.laneId ? `&laneId=${encodeURIComponent(item.laneId)}` : ""
}`,
);
break;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard against a missing sessionId before navigating.

If item.sessionId is falsy, sessionId defaults to "", producing /work?sessionId= — a degenerate URL that won't focus any session.

🔧 Suggested guard
         case "chat":
         case "terminal": {
-          const sessionId = item.sessionId ?? "";
+          const sessionId = item.sessionId;
+          if (!sessionId) break;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case "chat":
case "terminal": {
const sessionId = item.sessionId ?? "";
// The Work tab stays mounted (keep-alive), so its select-session
// listener focuses the target; the navigate switches the visible tab.
window.dispatchEvent(
new CustomEvent("ade:work:select-session", {
detail: { sessionId, laneId: item.laneId ?? undefined },
}),
);
navigate(
`/work?sessionId=${encodeURIComponent(sessionId)}${
item.laneId ? `&laneId=${encodeURIComponent(item.laneId)}` : ""
}`,
);
break;
}
case "chat":
case "terminal": {
const sessionId = item.sessionId;
if (!sessionId) break;
// The Work tab stays mounted (keep-alive), so its select-session
// listener focuses the target; the navigate switches the visible tab.
window.dispatchEvent(
new CustomEvent("ade:work:select-session", {
detail: { sessionId, laneId: item.laneId ?? undefined },
}),
);
navigate(
`/work?sessionId=${encodeURIComponent(sessionId)}${
item.laneId ? `&laneId=${encodeURIComponent(item.laneId)}` : ""
}`,
);
break;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/components/app/CommandPalette.tsx` around lines
1122 - 1138, Guard the chat/terminal branch in CommandPalette so it does not
navigate when item.sessionId is missing. In the switch case handling "chat" and
"terminal", check sessionId before dispatching "ade:work:select-session" and
calling navigate; if it is falsy, skip both actions or return early so
/work?sessionId= is never created. Use the existing sessionId, item.laneId, and
navigate logic in this branch to place the guard cleanly.

Comment on lines +361 to +378
const flatEntities = useMemo<FlatEntity[]>(() => {
const out: FlatEntity[] = [];
for (const section of sections) {
const expanded = expandedKinds.has(section.kind);
const visible = expanded
? section.rows
: section.rows.slice(0, ENTITY_SECTION_PREVIEW);
for (const item of visible) out.push({ type: "result", item });
if (!expanded && section.rows.length > ENTITY_SECTION_PREVIEW) {
out.push({
type: "showMore",
kind: section.kind,
hiddenCount: section.total - ENTITY_SECTION_PREVIEW,
});
}
}
return out;
}, [sections, expandedKinds]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

"Show N more" count can exceed what's actually available to render.

hiddenCount is derived from section.total (backend's true per-kind total), but visible/section.rows are capped by the shared SEARCH_QUERY_LIMIT across all kinds. When a kind's true total exceeds what fit in that shared page, expanding the section can reveal fewer new rows than the "Show N more" label promised, since expand doesn't trigger an additional fetch.

🔧 Suggested fix: clamp hiddenCount to what's actually fetched
-      if (!expanded && section.rows.length > ENTITY_SECTION_PREVIEW) {
-        out.push({
-          type: "showMore",
-          kind: section.kind,
-          hiddenCount: section.total - ENTITY_SECTION_PREVIEW,
-        });
-      }
+      if (!expanded && section.rows.length > ENTITY_SECTION_PREVIEW) {
+        out.push({
+          type: "showMore",
+          kind: section.kind,
+          hiddenCount: Math.min(section.total, section.rows.length) - ENTITY_SECTION_PREVIEW,
+        });
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const flatEntities = useMemo<FlatEntity[]>(() => {
const out: FlatEntity[] = [];
for (const section of sections) {
const expanded = expandedKinds.has(section.kind);
const visible = expanded
? section.rows
: section.rows.slice(0, ENTITY_SECTION_PREVIEW);
for (const item of visible) out.push({ type: "result", item });
if (!expanded && section.rows.length > ENTITY_SECTION_PREVIEW) {
out.push({
type: "showMore",
kind: section.kind,
hiddenCount: section.total - ENTITY_SECTION_PREVIEW,
});
}
}
return out;
}, [sections, expandedKinds]);
const flatEntities = useMemo<FlatEntity[]>(() => {
const out: FlatEntity[] = [];
for (const section of sections) {
const expanded = expandedKinds.has(section.kind);
const visible = expanded
? section.rows
: section.rows.slice(0, ENTITY_SECTION_PREVIEW);
for (const item of visible) out.push({ type: "result", item });
if (!expanded && section.rows.length > ENTITY_SECTION_PREVIEW) {
out.push({
type: "showMore",
kind: section.kind,
hiddenCount: Math.min(section.total, section.rows.length) - ENTITY_SECTION_PREVIEW,
});
}
}
return out;
}, [sections, expandedKinds]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/components/app/commandPaletteSearch.tsx` around
lines 361 - 378, The “Show N more” label in the command palette section preview
is using the backend’s total count instead of the number of rows actually
available in `section.rows`, so the displayed hidden count can overpromise.
Update the `flatEntities` memo in `commandPaletteSearch.tsx` to derive
`hiddenCount` from the fetched/visible data for each `section.kind` and clamp it
to what is actually present in `section.rows` (and the preview limit), so
expanding a section only advertises rows that can really be revealed without
another fetch.

… filters, listener unsubscribe on dispose, dispose ordering, palette sessionId guard + show-more clamp, --actions help

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@arul28

arul28 commented Jul 6, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 120708db0a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

removeSessionDocs(sessionId);
return;
}
if (isChatToolType(session.toolType)) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not index legacy chat sessions as terminals

When a legacy chat row has a chat JSONL transcript but a non-chat toolType such as other, processChatSession treats it as chat via isChatSession, but this guard lets the same session continue through terminal indexing. Because the terminal meta doc is written before checking for a .log, those legacy chats show up under kind:terminal and inflate terminal results even when no terminal transcript exists.

Useful? React with 👍 / 👎.

Comment thread apps/ade-cli/src/cli.ts

const queryArgs: JsonObject = { query };
if (kinds && kinds.length) queryArgs.kinds = kinds;
if (laneId) queryArgs.laneId = laneId;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve --lane names before passing laneId

The CLI help advertises --lane as accepting an id or name, but this stores the raw flag value in laneId; resolveLaneFilter returns args.laneId immediately and only resolves names for inline lane:<name> filters. As a result, ade search "auth" --lane fix-login filters FTS rows by d.lane_id = 'fix-login' and searches files with workspace id fix-login, so normal lane names return no lane-scoped matches unless they happen to equal the internal id.

Useful? React with 👍 / 👎.

Comment on lines +1173 to +1178
const root = project?.rootPath ?? null;
if (relative && root) {
const separator = root.includes("\\") ? "\\" : "/";
const absolute = `${root}${
root.endsWith(separator) ? "" : separator
}${relative}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Open lane-scoped file hits in the matched lane

For command-palette searches that include lane:<name>, the backend delegates file search to that lane's workspace, but activation re-roots the repo-relative result under the primary project.rootPath. In worktree lanes this opens the primary checkout's file (or a missing path) instead of the file that was actually matched in the lane; route the Files tab with the lane/workspace context rather than constructing a primary-root external path.

Useful? React with 👍 / 👎.

…als, lane-name resolution for --lane, lane-scoped file activation opens the lane worktree, since: RangeError guard

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@arul28

arul28 commented Jul 6, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b632559247

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +959 to +960
filters.push("(d.kind NOT IN ('chat', 'terminal') OR d.session_id = ?)");
filterParams.push(scopeChatSessionId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include chat-owned terminals in scoped search

When a non-CTO caller has a chatSessionId, this predicate only keeps terminal docs whose indexed session_id equals that chat id. Attached terminals store their own terminal row id as session_id during indexing, while the owning chat is held on TerminalSessionSummary.chatSessionId, and direct terminal reads authorize that owner path. In chats that launch separate shell/app-control terminals, scoped search.query therefore hides the caller's own terminal output; index and filter the owning chat id as well.

Useful? React with 👍 / 👎.

Comment on lines +2611 to +2612
if (event.type === "prs-updated") {
for (const pr of event.prs) searchServiceHolder.current?.notifyPrChanged(pr.id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Notify search from PR polling changes

This only refreshes PR search docs for prs-updated events, but the background polling path in this same file reports changed PR rows through createPrPollingService's onPullRequestsChanged callback rather than emitPrEvent. After the initial backfill, PR titles/bodies/comments updated by polling remain stale in universal search until some separate webhook or manual PR event happens; call notifyPrChanged for changedPrs in that polling callback too.

Useful? React with 👍 / 👎.

…hat's scoped caller (schema v2 owner_session_id), PR polling notifies search in both hosts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@arul28

arul28 commented Jul 6, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 55a5e9b633

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/ade-cli/src/adeRpcServer.ts Outdated
// an unbound agent/orchestrator/evaluator — which chat.readTranscript
// would deny outright — gets no session content at all.
if (session.identity.role === "external") return searchArgs;
return { ...searchArgs, callerScope: { excludeSessionContent: true } };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat unbound CLI search as whole-project

For a normal ade search launched from a shell, the CLI initializes as role agent by default and usually has no ADE_CHAT_SESSION_ID, so this branch injects excludeSessionContent and strips all chat and terminal hits. That makes the documented user CLI search miss the primary indexed content unless users know to pass --role external; distinguish unbound human CLI callers from unbound agents or have the search command initialize as an external/user caller.

Useful? React with 👍 / 👎.

delegated.push(...(await delegatedLaneCandidates(parsed, matchAll)));
}
if (kinds.includes("file")) {
delegated.push(...(await delegatedFileCandidates(parsed, matchAll, laneId)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor since filters for file results

When the query includes since:..., the FTS, lane, and artifact paths apply parsed.sinceIso, but this still delegates file search and returns file hits with updatedAt: "". A default query like ade search "TODO since:7d" can therefore be filled with file matches that are not known to be recent, violating the recency filter; skip or timestamp file results when parsed.sinceIso is set.

Useful? React with 👍 / 👎.

… search (plain ade defaults to role agent; bound workers stay scoped), since: filter omits timestamp-less file results

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@arul28

arul28 commented Jul 6, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/ade-cli/src/adeRpcServer.ts`:
- Around line 2350-2358: The unbound search behavior in adeRpcServer now allows
whole-project results, but the shared callerScope contract still documents
unbound non-external agent roles as excluding session content. Update the shared
type/comment in search.ts to match the new unbound behavior, or change the
searchArgs handling in the callerScope path to keep excludeSessionContent
enforced; keep the contract and the implementation aligned.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9945c2f0-f96d-40c2-b337-cc0d75827a61

📥 Commits

Reviewing files that changed from the base of the PR and between 55a5e9b and 78b445c.

📒 Files selected for processing (4)
  • apps/ade-cli/src/adeRpcServer.test.ts
  • apps/ade-cli/src/adeRpcServer.ts
  • apps/desktop/src/main/services/search/searchService.test.ts
  • apps/desktop/src/main/services/search/searchService.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/desktop/src/main/services/search/searchService.test.ts
  • apps/desktop/src/main/services/search/searchService.ts

Comment thread apps/ade-cli/src/adeRpcServer.ts

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 78b445c359

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

rebuildIndex(): SearchRebuildResult {
if (disposed) return { started: false };
queue.clear();
clearSearchIndex(ensureDb().db);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Serialize rebuilds with in-flight indexing

When search.rebuildIndex runs while the background workChain is already processing a source, this clears the DB immediately instead of waiting for that in-flight processor to finish. A chat/terminal processor that read an old cursor can then resume after the clear, write only the later transcript chunk, and persist an advanced cursor; the new backfill is chained behind it and will see that cursor, so earlier transcript bytes stay missing from the rebuilt index until another idle rebuild.

Useful? React with 👍 / 👎.


const startBackfill = (): void => {
if (backfillStarted || disposed) return;
backfillStarted = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reset backfill state after startup failures

If any awaited source list in the delayed startup backfill throws once (for example sessions/lanes/PRs are temporarily unavailable during project startup), backfillStarted remains true after the catch logs the failure. Later startBackfill() calls then return immediately, so any sources not enqueued before the failure are never indexed unless the user explicitly runs a rebuild.

Useful? React with 👍 / 👎.

…exing, backfill retry after startup failure, callerScope contract comment sync

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@arul28

arul28 commented Jul 6, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 223688722c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// ---------------------------------------------------------------------

const effectiveKinds = (args: SearchQueryArgs, parsed: ParsedSearchQuery): SearchDocKind[] => {
const fromArgs = (args.kinds ?? []).filter(isSearchDocKind);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject invalid kind filters before falling back

When search.query is invoked through the ADE action bridge or preload with a malformed kinds array, this line drops every invalid value; if none remain, effectiveKinds falls through to the default all-kind set. For example, ade actions run search.query --input-json '{"query":"secret","kinds":["termnal"]}' returns chat/PR/file/etc. hits instead of an empty or invalid-kind response, so a caller's intended narrowing can silently broaden results. Preserve whether args.kinds was supplied and reject or return no results when any requested kind is not valid.

Useful? React with 👍 / 👎.

title: session.title,
rankTitle: null,
snippetSource: sanitized.slice(0, 240),
deepLink: withQueryParam(baseLink, "event", seq),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Carry hit offsets through session deeplinks

These event deep links are advertised as opening the exact chat message, and terminal chunks add the same kind of offset parameter, but the shared ADE deeplink parser only preserves lane for ade://session/... before dispatching a Work navigation target. In the ade search --kind chat --json / ade open <deepLink> flow, the app therefore opens the session but drops the matching message/scrollback position, so the result cannot navigate to the hit. Extend the session deeplink target/navigation payload to carry these parameters or avoid emitting unsupported anchors.

Useful? React with 👍 / 👎.

laneId,
laneName: null,
sessionId: null,
deepLink: `ade://files?path=${encodeURIComponent(item.path)}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Emit file links that the app can open

File search hits return ade://files?..., but the shared ADE deeplink parser/dispatcher only recognizes lane, session, branch/repo, PR, and Linear issue targets. In the CLI/skill flow (ade search --kind file --json followed by ade open <deepLink>), these links parse as unknown_type and do nothing, so file results cannot be opened from their advertised deepLink. Add a supported file target to the deeplink parser/navigation path or return an existing openable route.

Useful? React with 👍 / 👎.

@arul28

arul28 commented Jul 6, 2026

Copy link
Copy Markdown
Owner Author

Merging via ship loop (force-finalize at iteration cap; ci-pass green on 2236887). Three P2 polish items from the final Codex round are deliberately deferred as follow-ups:

  1. search.query with an all-invalid kinds array falls back to the default kind set instead of rejecting (inline kind: filters already reject).
  2. Chat/terminal deepLink anchor params (event=/offset=) are emitted but not yet honored by the ade open deeplink parser — the desktop palette navigates via typed fields, so in-app navigation is unaffected.
  3. ade://files?... deepLinks are not parseable by ade open (no file target in the shared parser yet).

🤖 Generated with Claude Code

@arul28 arul28 merged commit 38a9610 into main Jul 6, 2026
32 of 33 checks passed
@arul28 arul28 deleted the ade/universal-search-palette-527927bf branch July 6, 2026 18:57
@arul28 arul28 mentioned this pull request Jul 7, 2026
@arul28

arul28 commented Jul 7, 2026

Copy link
Copy Markdown
Owner Author

All three deferred items are fixed in PR #713: (1) supplied-but-all-invalid kinds arrays now return empty results, (2) event=/offset= anchors are parsed and honored end-to-end by ade open (chat scroll-to-message + terminal best-effort), (3) file deeplinks are a first-class ade://file/... target in the shared parser, emitted canonically by search.

arul28 added a commit that referenced this pull request Jul 7, 2026
…main

The lane-branch-reset incident left three files rebuilt from a pre-#710 base,
which read to git as intentional reverts of relay-everywhere code (Greptile
P1s: relay URL missing from handshake, non-host runtime owning the relay).
3-way remerge (base=#709, ours=main, theirs=branch) so main's relay behavior
returns while the deeplink additions stay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
arul28 added a commit that referenced this pull request Jul 7, 2026
* search: reject supplied-but-all-invalid kinds arrays instead of broadening

A query whose kinds array contains only invalid entries (e.g. ["termnal"])
previously fell back to the default all-kind set — silently broadening the
search. Mirror the inline kind:bogus reject: supplied + nothing valid left
means empty results.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* deeplinks: session anchors, file target, portable envelopes + resolution ladder (desktop)

- Shared contract: session links carry event=<seq> (chat) / offset=<bytes>
  (terminal) anchors; new file target (repo-relative path + line + lane);
  new commit/artifact targets; portable envelope (repo/branch/pr/linear
  query params) on machine-local targets, parsed leniently.
- Anchors consumed end-to-end: chat list scrolls to the anchored event
  (envelope.sequence match, ordinal fallback on full history, bounded
  older-history paging, highlight); terminal replay scrolls by byte
  fraction when the whole transcript is loaded; Files editor reveals the
  anchored line. Search emits envelope.sequence in chat links and
  canonical ade://file links (index schema 2->3, disposable rebuild).
- Resolution ladder for session/lane links: local -> open exactly;
  repo matches another known project (new project.findForRepo IPC +
  repoProjectResolver over recents' git configs) -> switch-project card;
  foreign machine -> fallback-only card (create lane from branch, open
  PR on GitHub, open Linear issue). InboundDeeplinkModal generalized;
  no request-access path by design.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* deeplinks(ios+web): router parity, Universal Links, envelope-aware Send-to-Mac, link minting

- https /open session links navigate locally on the phone; file/commit/
  artifact shapes validate then route to Send-to-Mac; session anchors
  carried on WorkSessionNavigationRequest (no scroll hook yet on iOS).
- Universal Links: applinks:ade-app.dev entitlement, NSUserActivity
  routing, AASA served from apps/web/.well-known (appID
  VQ372F39G6.com.ade.ios, /open* and /pair* only) with JSON content-type.
- Envelope-aware Swift link builder; lane/session/PR copy-link actions
  mint https links with envelopes; Send-to-Mac card offers GitHub PR /
  Linear fallbacks from the envelope. Web /open page describes the new
  target types.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* deeplinks: commit/artifact dispatch, ade link targets, envelope auto-embed sweep

- commit target dispatches to the owning lane (commitSha param) with the
  same resolution ladder as sessions/lanes; foreign fallback offers the
  GitHub commit URL. artifact links open the history surface (no focused-
  artifact mechanism exists yet). Palette commit results deep-route too.
- ade link gains file/commit/artifact targets plus mint-time envelope
  enrichment (lane branch, PR number, Linear issue) with --no-envelope;
  round-trip re-emit handles all new forms. ade-deeplinks skill doc updated.
- Auto-embed: Linear lane cards and desktop copy-link affordances (lane
  context menu, session copy, TUI Ctrl+Y) now mint envelope-carrying links.
  Search emits canonical commit/artifact links and repo/branch envelopes on
  session links via an optional repoSlug dep (index schema 3->4).
- Recovered from an out-of-process lane-branch reset that wiped the working
  tree mid-task: restored A/B content (kinds guard, sequence anchors,
  canonical file links, palette anchor pass, regression tests) and re-added
  the named findForRepo types. Backup refs: backup/pre-incident,
  backup/c-snapshot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(deeplinks): new targets, anchors, portable envelopes, resolution ladder, iOS universal links

Also restores the contract-file doc comments and the parser edge-case tests
(traversal normalization, malformed anchors, envelope leniency).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* quality: traversal guard on RPC file targets, anchor re-apply, commit-sha retry, envelope/link hygiene

Dual-review synthesis fixes:
- app/navigate RPC file targets now validated with the shared repo-relative
  rules (exported isValidRepoRelativePath/isValidCommitSha) + defense-in-depth
  check in renderer dispatch — an RPC caller could previously smuggle
  ../../ paths past the URL parser into openExternalPath.
- Work URL anchor apply-once guard keyed by session AND event/offset so
  re-opening the same session at a different anchor re-positions.
- Commit deeplink keeps laneId+commitSha through switch-project retries
  instead of dropping to a bare /lanes.
- Lane search results now carry the portable envelope (envelopeForLane).
- ade link refuses to mint links the shared parser would reject (round-trip
  gate).
- project.findForRepo IPC rewired through the tested mtime-cached resolver
  (was an inline reimplementation spawning git per recent project).
- Parser branch/pr assembly unified into shared helpers across both URL forms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: regression pins, folder consolidation, parity residuals

- Pin the quality gate: app/navigate file-traversal + malformed-sha rejection
  (adeRpcServer), ade link round-trip refusal (CLI), lane search envelope
  (searchService).
- Consolidate services/deeplinks to one test file (dispatch contract merged
  into protocolHandler.test.ts).
- Parity residuals: search README deepLink prose, ios-companion universal-link
  notes, ade-cli README CLI-surface inventory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ship: dedupe AASA header after main merge

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ship: restore #710 relay code accidentally reverted in bootstrap/cli/main

The lane-branch-reset incident left three files rebuilt from a pre-#710 base,
which read to git as intentional reverts of relay-everywhere code (Greptile
P1s: relay URL missing from handshake, non-host runtime owning the relay).
3-way remerge (base=#709, ours=main, theirs=branch) so main's relay behavior
returns while the deeplink additions stay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ship: address Codex P2s — repo-qualified PR links hand off to Mac on iOS; validate ade link file paths before building

- iOS: https pr links carrying a repo param no longer resolve a local PR by
  bare number (numbers are only unique per repo and the snapshot has no repo
  identity) — they route to Send-to-Mac where the desktop ladder verifies.
- ade link file validates the input path with the shared repo-relative rule
  BEFORE building: the ade:// path form URL-normalizes dot segments, so the
  post-build round-trip gate alone would accept ../secret as a link to a
  different in-repo path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ship: sweep stale-state dispatch class — latest-dispatcher ref, no root fallback for lane file links

- The switch-project card now re-dispatches through a ref that always points
  at the latest dispatcher, so post-switch retries see the new project's
  lanes/root instead of the closure captured before switchToPath.
- laneById / active-repo / findForRepo comparisons read via refs (same class).
- File deeplinks with an explicit lane never degrade to the project root:
  refresh + bounded wait for the lane list, then open the Files tab bare (or
  report unhandled during suppressed retries) instead of composing the same
  relative path in the wrong worktree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants