Skip to content

feat: introduce agentic search pipeline with live trace streaming#100

Merged
quiet-node merged 69 commits intomainfrom
feat/search-feature-v3
Apr 22, 2026
Merged

feat: introduce agentic search pipeline with live trace streaming#100
quiet-node merged 69 commits intomainfrom
feat/search-feature-v3

Conversation

@quiet-node
Copy link
Copy Markdown
Owner

@quiet-node quiet-node commented Apr 21, 2026

Summary

This PR introduces a full agentic web search pipeline into Thuki, activated via the `/search` command. The implementation spans new Docker sandbox services, a Rust search engine, and frontend trace/warning UI.

New Infrastructure: `sandbox/search-box`

Two new containerized services join the existing `llm-box` (Ollama), now cleanly namespaced under `sandbox/search-box/`:

SearXNG (meta-search engine)

  • Self-hosted privacy-respecting search aggregator configured via `settings.yml`
  • Locked down: no public endpoints, localhost-only binding, results deduplicated across engines
  • Configured for parallel fanout across multiple search backends

Reader (custom Python HTTP service)

  • FastAPI service wrapping Trafilatura for clean article extraction
  • Strips boilerplate, ads, nav elements from raw HTML; returns plain text for LLM consumption
  • Dockerfile: slim Python base, non-root user, minimal surface area
  • Fully tested (`test_main.py` covers failure branches, non-JSON decode, error responses)

Sandbox reorganization

  • Existing Ollama compose moved from `sandbox/` root into `sandbox/llm-box/` for symmetry
  • `sandbox/search-box/docker-compose.yml` brings up both SearXNG + Reader as a unit

New Backend: Agentic Search Pipeline (`src-tauri/src/search/`)

A new Rust module implementing a multi-round retrieval loop:

Module Role
`config.rs` Loop caps, timeouts, TOP_K_URLS, MAX_ITERATIONS constants
`searxng.rs` Parallel query fanout to SearXNG with URL deduplication
`reader.rs` HTTP client to the Reader service, fetches and cleans article text
`chunker.rs` Splits reader output into overlapping chunks for BM25 scoring
`rerank.rs` BM25 chunk-level reranker; surfaces most relevant passages
`judge.rs` LLM-powered sufficiency judge: decides if current context answers the query
`llm.rs` All Ollama chat calls: router, judge, synthesis, with retry+fallback on JSON hallucination
`pipeline.rs` Orchestrates the full agentic loop: plan → search → read → rerank → judge → refine or synthesize
`probe.rs` Sandbox health check: verifies SearXNG and Reader are reachable before running
`errors.rs` Typed error hierarchy covering transient vs. permanent failures
`types.rs` Shared types: `SearchEvent`, `SearchWarning`, `Verdict`, trace variants

Agentic loop behavior

  1. LLM generates a search plan (decomposed sub-queries)
  2. Parallel SearXNG fanout fetches results for all sub-queries
  3. Top URLs fetched via Reader, chunked, BM25-reranked
  4. Sufficiency judge decides: `sufficient` (synthesize) or `gap` (refine and loop)
  5. Loop is bounded by `MAX_ITERATIONS`; exhaustion triggers `IterationCapExhausted` warning
  6. Final synthesis cites sources by index

LLM robustness

  • Router and judge calls use retry-once + fallback on malformed JSON (fights LLM hallucination)
  • `call_judge` handles unbalanced braces and non-JSON wrapping in model output

Prompts added

  • `search_plan.txt` (router): decomposes query into parallel sub-queries
  • `search_judge.txt` (sufficiency judge): structured verdict with gap analysis
  • `search_synthesis.txt`: citation-anchored synthesis with substantive-answer enforcement

Frontend Changes

  • Live trace streaming: each pipeline stage (`AnalyzingQuery`, `FetchingUrl`, `Reranking`, `Synthesizing`, etc.) streams to the UI in real time via the existing Tauri Channel
  • Search warning system: warnings accumulate per turn (e.g. `IterationCapExhausted`, `ReaderTimeout`); severity levels map to icon color and tooltip copy
  • Warning icon: appears on search turns with a shared `Tooltip` component, pointer cursor
  • Round-aware stage labels: UI shows "Refining (round N)" during gap-analysis iterations
  • New icons: `/search` and `/translate` command suggestions get distinct icons with tooltips
  • DB persistence: search warnings and metadata stored per turn in SQLite via `history.rs`

Test Coverage

Suite Tests Coverage
Frontend (Vitest) 721 100% lines/branches/functions/statements
Backend (Cargo) 381 unit + 5 e2e 100% line coverage enforced by llvm-cov
Reader service (pytest) covered failure paths, decode errors, thin wrappers

E2E integration tests (`tests/search_pipeline_e2e.rs`) exercise the full agentic loop against a live sandbox.

Test Plan

  • All 721 frontend tests pass (100% coverage)
  • All 381 backend unit tests pass (100% line coverage enforced by llvm-cov)
  • 5 end-to-end integration tests for agentic loop pass
  • Reader service tests pass
  • All linting, formatting, and build validation complete with zero errors
  • Spin up `sandbox/search-box` with `docker compose up` and run a `/search` query end-to-end
  • Verify live trace blocks appear during search and warning icon shows on capped iterations

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
… thinking animation fixes

Consolidate loading indicators into shared LoadingStage component with shimmer-sweep animation applied to both /search and /think stages. Fix per-character thinking animation (resolve invisible text bug). Implement collapsible sources list with letter-avatar sources, two-way hover for inline citations, and SQLite persistence of sources. Refactor pill-style indicators to minimal list and letter-avatar typography. Update slash command config and add comprehensive tests for search features.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…le dates

The 4B synthesis model was emitting dates from its training cutoff
(e.g. "as of September 2025") instead of grounding on the fetched
sources. We now compute the current UTC date once per /search turn and
inject it into the synthesis system prompt, plus explicit rules:
never state a date not in the sources, prefer the most recent source
date when multiple differ, and treat the sources as the current state
of the world.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Fuse a BM25F lexical score over the retrieved-set corpus with the
upstream SearXNG engine order via Reciprocal Rank Fusion (k=60). Title
weighted 2x content; Lucene defaults k1=1.2, b=0.75. Pure Rust, no new
dependencies, no unsafe, bounded O(N*T) per call. The rerank runs after
the SearXNG fetch and before the Sources event, so the frontend footer
and the synthesis prompt observe the same order.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
New FastAPI service in sandbox/search-box/reader that extracts
LLM-ready markdown from arbitrary URLs. Private-host guard, byte
cap, and run-as-non-root are in place per the hardened sandbox
pattern. See design doc for rationale.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Moves pytest out of the production requirements file so it is not
baked into the Docker image. Runtime deps stay in requirements.txt;
pytest lives in requirements-dev.txt for local test runs.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Binds localhost only on port 25018, drops all capabilities,
read-only rootfs with a small tmpfs for /tmp, mem and cpu caps.
Shares the existing search-box network so the reader can be
reached only from the same sandbox context.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…er timeout

Inner urllib timeout bumped from 2s to 4s to sit just under the
Docker healthcheck timeout of 5s, eliminating a small inconsistency
between the two timeouts. The 1s buffer preserves clean process
exit within Docker's window.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Introduces Action, Sufficiency, RouterJudgeOutput, JudgeVerdict,
SearchWarning, IterationStage, IterationTrace, and SearchMetadata.
Extends SearchEvent with AnalyzingQuery, ReadingSources,
RefiningSearch{attempt,total}, Composing, and Warning variants.
Preserves existing Classifying/Clarifying/Sources/Token variants so
the pipeline keeps compiling; they are removed when Task 13 updates
call sites. New types are re-exported from search/mod.rs to eliminate
dead_code warnings while the pipeline tasks that consume them are
still pending.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Centralizes MAX_ITERATIONS=3, GAP_QUERIES_PER_ROUND=3, TOP_K_URLS=5,
CHUNK_TOKEN_SIZE=500, TOP_K_CHUNKS=8, plus reader and judge
timeouts. Constants only; no behavior change yet.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Introduces search_judge.txt with the discrete three-level
sufficiency schema plus gap_queries. Synthesis prompt gains a
single sentence clarifying that citation markers apply equally to
search-result snippets and reader-extracted chunks. Router prompt
is untouched in this commit; it is rewritten together with the
merged call signature in Task 11.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Bounded single retry primitive for LLM, SearXNG, and reader calls.
String-level transient classifier avoids threading concrete error
types through every call site. No exponential backoff: semantics
are hiccup recovery, not generic retry.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Splits reader markdown into target-sized chunks respecting paragraph
boundaries. Preserves source URL and title per chunk so citations
remain correct across snippet- and chunk-level inputs to the judge.
Word-level approximation for token counts keeps the chunker
decoupled from any specific model tokenizer.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Reuses the rerank module for chunk-level scoring with vanilla
Okapi BM25 (no RRF since chunks have no secondary ordering to
fuse with). Stable ordering for ties via original-index
tiebreaker. Returns up to top_k chunk references.

Refactors the existing private tokenize() into tokenize_uncapped()
+ a capped wrapper so chunk text (up to ~500 words) is tokenized
without truncation while query strings retain the MAX_QUERY_TOKENS
defence-in-depth cap. Unicode-aware lowercasing is preserved.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Tolerant JSON extraction strips chatty preamble and markdown
fences. Normalizer caps gap_queries to N and clears them when the
verdict is sufficient, enforcing prompt invariants in code so the
pipeline does not rely solely on the model obeying its system
prompt.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Concurrent per-URL fetches with a 5-slot semaphore, per-batch
timeout, cancellation via tokio::select, and a single retry on
transient connect errors. Reports empty-body URLs separately so the
pipeline can log them for the future Playwright decision, and
returns ServiceUnavailable when the reader sidecar is entirely
unreachable so the caller can degrade to snippets.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Introduces ROUTER_MERGED_SYSTEM_PROMPT (new prompt file) and the
companion call_router_merged returning RouterJudgeOutput in a single
Ollama roundtrip. Adds call_judge that drives the universal
sufficiency check for both snippet and chunk rounds, wiring
judge::parse_verdict and judge::normalize_verdict so invariants are
enforced in code. Existing call_router and ROUTER_SYSTEM_PROMPT are
untouched; Task 13 swaps the pipeline over and Task 16 retires them.

Refactors the internal HTTP wiring into a shared request_json helper
used by all three LLM call functions. Adds SearchError::Router and
SearchError::Judge variants to types.rs for typed error propagation.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
search_all issues N queries concurrently, collects results, and
returns the URL-deduplicated union. Individual query failures are
tolerated so a flaky single query in a gap round does not take out
the rest of the batch.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Introduces RouterJudgeCaller and JudgeCaller traits plus a
run_agentic entry point that handles the CLARIFY and
history-sufficient short-circuit branches. Non-sufficient PROCEED
returns an internal error pending Task 14 which wires the search
round, reader escalation, and chunk-level judge. Legacy run is
untouched; the Tauri command still dispatches there until Task 16
retires it.

Also adds SearchError::Internal(String) for stub-boundary returns
and split_into_stream_pieces for streaming clarifying questions as
Token events. DefaultRouterJudge/DefaultJudge are stubbed with
unimplemented!() because per-call dependencies (endpoint, model,
client, cancel token) are not injectable at struct level; they will
be wired in Task 16 when the Tauri command swaps.

Deviation from spec: CLARIFY branch persists the question to history
(same as legacy run_clarify_branch) so subsequent turns can see the
clarifying exchange.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Replaces the SearchError::Internal stub with the full initial
iteration: SearXNG, URL-level rerank, snippets judge, reader
escalation, chunking, chunk rerank, chunks judge. On sufficient
verdicts at any stage, streams synthesis and returns. On
insufficient, falls to the exhaustion fallback for now; Task 15
adds the gap loop. Graceful degradation on reader-unavailable and
reader-partial-failure paths emits warnings without aborting.

run_agentic gains searxng_endpoint and today parameters (matching
the legacy run signature) to make SearXNG and synthesis testable
without thread_local overrides. The MockJudge stub is replaced by
QueueJudge; the three Task 13 "Internal error" stub tests are
retired and replaced by five Task 14 scenario tests.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Add reader_base_url parameter to run_agentic (matching the
searxng_endpoint pattern) so tests can inject a mock reader server and
exercise the full reader escalation path without touching the production
READER_BASE_URL constant.

Set READER_BATCH_TIMEOUT_S=1s in test builds via cfg(test) so the
BatchTimeout error path is testable without waiting 30 seconds.

Add tests covering: mid-CLARIFY cancellation, before-SearXNG
cancellation, SearXNG HTTP error propagation, reader Cancelled path,
reader BatchTimeout path, reader majority HTTP failure partial warning,
and QueueJudge empty queue error. All new pipeline.rs production lines
now show zero uncovered regions in the llvm-cov HTML report.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Loops up to MAX_ITERATIONS after the initial round. Each gap round
emits RefiningSearch { attempt, total } before Searching, reuses
accumulated URLs and chunks, and respects the cancellation token.
Empty-result gap rounds advance the counter silently. Warning
dedup via contains-checks ensures ReaderUnavailable and
ReaderPartialFailure do not repeat across rounds. Exhaustion
synthesis uses the best accumulated chunks with the
IterationCapExhausted warning.

Adds search_all_with_endpoint to searxng so the gap loop can fan
out parallel gap queries using the already-resolved endpoint URL.

Updates the Task 14 exhaustion test to use insufficient_verdict_no_gaps
so the loop exits on the empty-queries guard without needing SearXNG
mocks. Adds four new gap-loop tests: gap round succeeds within cap,
all iterations exhaust, empty SearXNG breaks loop silently, and
ReaderUnavailable dedup across rounds.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Wires DefaultRouterJudge and DefaultJudge to call_router_merged and
call_judge respectively. Tauri search_pipeline command now
dispatches to run_agentic, making the agentic loop the production
/search implementation. Legacy run, call_router, run_search_branch,
and the ROUTER_SYSTEM_PROMPT prompt file are removed along with
their tests. persist_turn accepts warnings and metadata arguments;
DB column migration lands in Task 17. Cancellation is now checked
at every stage entry and races inline against long SearXNG and
judge HTTP calls.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Adds search_metadata and search_warnings TEXT columns to the
messages table via idempotent ensure_column calls at startup.
Refactors the three existing PRAGMA-based migrations to use the
same helper so all column additions follow one pattern.

Extends insert_message, insert_messages_batch, load_messages, and
PersistedMessage with the two new optional JSON TEXT fields. Adds
search_warnings and search_metadata to SaveMessagePayload and
persist_message so the frontend can pass serialized values when
saving a search turn.

The pipeline persist_turn parameters are renamed from _warnings/_metadata
to warnings/metadata; they are acknowledged via a let _ binding since
the pipeline owns no DB connection. DB population happens through
the frontend save flow added here.

Adds three unit tests: ensure_column_is_idempotent,
persist_and_load_round_trip_includes_warnings_and_metadata, and
persist_and_load_tolerates_null_search_metadata.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Extends SearchEvent with AnalyzingQuery, ReadingSources,
RefiningSearch{attempt,total}, Composing, and Warning variants.
Removes Classifying and Clarifying (dropped per design decision
15: clarifying questions now stream as Token events). Adds
SearchWarning union matching the Rust enum with snake_case
values. SearchStage is now a discriminated union so the UI can
render the refining-search attempt counter.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Const maps from SearchWarning enum to friendly user-facing string
and warn or error severity. Consumed by the warning icon (Task 21)
and the bubble border rule. Kept in config so wording can be tuned
without editing component code.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…ounter

Replaces "Classifying query" with "Analyzing query" per the locked
UX copy. Refining-search stage now renders the attempt counter
"Refining search (k/N)" from the RefiningSearch event fields.
SEARCH_STAGE_LABELS const replaced with a helper that handles the
counter case.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Subtle 14px icon renders beside the Sources collapsible when a
turn carries warnings. Amber triangle for warn-only warning lists,
red circle when any warning is error severity. Native title
tooltip stacks friendly copy lines. Renders nothing when the
warnings list is empty.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
After the first RefiningSearch event fires, subsequent Searching /
ReadingSources / Composing stages now show copy that conveys Thuki
is looking at more material rather than repeating the initial
linear flow.

Labels:
- Initial round: Searching the web, Reading sources, Composing answer
- Gap rounds:   Searching more angles, Reading additional pages,
                Composing refined answer

Implementation stays on the frontend: useOllama tracks an
inGapRound flag that flips on the first RefiningSearch event and
propagates to subsequent stage objects via an optional gap field.
ConversationView picks the label variant per stage. No backend
changes; existing tests updated to expect gap: true for post-
RefiningSearch stages.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
The previous prompt combined Answer directly in the first sentence
with One to three short paragraphs is usually enough. Small local
models followed this literally and produced one-sentence answers
to entity-style questions (who founded Twitter, etc.), forcing the
user into manual follow-ups.

New guidance matches the industry standard for research-search
answers (Perplexity, ChatGPT Search, You.com):

- Open with the direct literal answer.
- Follow with the supporting context a curious reader would want
  next, picked by question type (person, company, event, process,
  comparison).
- Anticipate the obvious follow-up so one-liners are not accepted
  when the sources supply more.
- Target two to four tight paragraphs by default, scaling to the
  question complexity. Never pad.

Every grounding guardrail stays: no preamble, no meta commentary,
no hallucinated dates, no uncited claims, paraphrase not copy,
match the users language.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…y answers

Two changes addressing one-sentence answers like 'Jack Dorsey, Noah
Glass, Biz Stone, and Evan Williams founded Twitter' for rich
entity queries.

1. Judge prompt tightened:
   - sufficient now requires BOTH the literal answer AND the
     supporting facts a substantive answer needs (dates, numbers,
     context). Bare snippets no longer count as sufficient for
     entity / event / comparison / how-to questions.
   - partial is the expected verdict when snippets give the literal
     answer but miss supporting context. This triggers reader
     escalation so synthesis sees full-page content instead of a
     150-char snippet.
   - gap_queries guidance clarified for partial: target the missing
     supporting facts, not the literal answer again.

2. Synthesis prompt gains a concrete few-shot example for 'Who
   founded Tesla?' showing an unacceptable one-liner next to an
   acceptable grounded two-sentence version. Small local models
   (default here is gemma4:e2b) follow shown examples far more
   reliably than abstract substance instructions.

No code changes, no test changes. Runtime verification is smoke
tests against the live pipeline.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…tains the answer

The previous prompt allowed history_sufficiency=sufficient when
the query was 'stable general knowledge as of today'. Small local
models interpreted this aggressively and answered basic questions
(for example who founded Twitter) from training data alone,
producing a one-liner with no citations and no Sources footer.

Users who invoke /search explicitly want fresh web data. The
router should only short-circuit when the prior turns literally
contain the answer already. Training knowledge and general-
knowledge carve-outs are both removed.

Key changes:

- Opening line reframes the router's job: default is a fresh
  search; only short-circuit when the transcript has the answer.
- sufficient now requires a prior turn to literally contain the
  fact the user is asking about; training knowledge explicitly
  does NOT count.
- Default posture is insufficient. Bias toward insufficient for
  time-sensitive topics: ownership, pricing, versions, roles,
  statuses, current events.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…o system prompt

Adds docs/agentic-search.md: a deep technical tour of the /search
pipeline covering every stage (router+judge merged call, SearXNG,
BM25F+RRF rerank, sufficiency judge, Trafilatura reader, chunker,
chunk rerank, gap loop, synthesis), security posture at all three
container layers, graceful-degradation behavior, industry
comparison against Perplexity / Exa / CRAG / Self-RAG / FLARE /
Adaptive-RAG, configuration knobs, performance characteristics,
and a canonical file map. Written to work for readers at any
technical level.

Updates src-tauri/prompts/system_prompt.txt with a new
'Your /search capability' section so Thuki can accurately explain
its own search command when asked, suggest it when a query needs
fresh web data, and stay honest about its limits. Also replaces a
pre-existing em dash in the conversational-continuity section with
parenthetical clauses per the project style rule.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Rename src-tauri/prompts/search_router_merged.txt to search_plan.txt and
update the include_str! path in llm.rs. Rename the public constant
ROUTER_MERGED_SYSTEM_PROMPT to SEARCH_PLAN_SYSTEM_PROMPT and update all
references in llm.rs (usage site, doc comment, test). Update the file
name reference in docs/agentic-search.md. No content change in the prompt.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…ERATIONS without sufficient

Previous attempt set a flag based solely on attempt == MAX_ITERATIONS, which missed the case where the flag had been set then the loop exited early via current_queries.clear() on the NEXT iteration. The warning still fired despite the loop not actually running every round to completion.

Correct fix: track hit_iteration_cap and set it only AT THE BOTTOM of the loop body (after the judge call and the Sufficient early-return) when attempt equals MAX_ITERATIONS. Every early-exit path (empty current_queries guard at the top, empty SearXNG new_urls clearing current_queries, cancellation, Sufficient verdict early return) bypasses the flag assignment.

Tests updated and added for every exit path:
- real cap hit across all three iterations with Insufficient plus gap queries: warning fires
- initial round Insufficient with no gap queries: no warning
- gap round SearXNG returns only seen URLs: no warning
- Sufficient at attempt 2: no warning
- Sufficient at attempt 3: no warning
- attempt 3 runs in full with Insufficient and empty gap queries: warning fires (cap reached)

Single clean commit replacing the earlier 2fdc3cb which did not cover all exit paths.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
.gitignore: add *.profraw so coverage runs do not leave instrumentation artifacts in the working tree.

llm.rs: collapse SEARCH_PLAN_SYSTEM_PROMPT include_str to a single line after the rename commit 8c1ee7a left it split on two lines with trailing whitespace that rustfmt would otherwise re-flow.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Add a concurrent healthz probe that runs before the search pipeline. If
SearXNG or the reader container is unreachable, the backend emits a
SandboxUnavailable event instead of a cryptic HTTP error. The frontend
handles the event in useOllama, renders a SandboxSetupCard with Docker
Compose setup guidance, and ChatBubble routes the card into chat.

All frontend and backend coverage remains at 100%. Closes the branch
coverage gaps in useConversationHistory (searchWarnings serialisation)
and ConversationView (gap-mode label ternaries) introduced on this branch.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…source filtering

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
When the judge LLM returns malformed or non-JSON output, the pipeline
previously propagated SearchError::Judge and aborted. Mirror the
retry+fallback pattern already in call_router_merged: retry once with a
stricter prompt suffix, then fall back to Partial + empty gap_queries so
the pipeline synthesizes from existing evidence instead of surfacing a
cryptic error.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Two issues fixed:
- src-tauri/src/search/mod.rs: Pass config::READER_BASE_URL (port 25018) instead of searxng::SEARXNG_BASE_URL (port 25017) for reader /extract calls. The wrong base URL caused all URLs to show as "failed" in the trace.
- src-tauri/src/search/pipeline.rs: Update trace messaging for clarity: "opened" -> "read", "Opening the shortlisted pages" -> "Reading the shortlisted pages", "full text behind" -> "full text from", "could be opened cleanly" -> "could be read cleanly".
- src/components/SearchTraceBlock.tsx: Update chip label from "opened" to "read" to match pipeline messaging.
- Test files: Updated assertions to match new strings.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…and /translate

- Wrap each command description in Tooltip so full text shows on hover
- Tooltip gains optional className prop for flex layout integration
- /search gets a globe icon (web search)
- /translate gets a 文A icon matching Google Translate icon style
- Shorten /search description to fit without truncation

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…end regressions

- added unit tests for search pipeline error paths and pluralization logic
- achieved 100% line coverage in src-tauri/src/search/pipeline.rs
- implemented scrollIntoView for command suggestions with JSDOM mocks
- fixed formatting and lint warnings in Tooltip component

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…atches! with explicit match arms in tests to fix llvm-cov attribution - Implement TcpListener mock for mid-stream network error test - Add 100% line coverage enforcement to test:all:coverage script - Refactor frontend tests to use act() and fake timers where missing - Update search metadata types and persistence logic for agentic v3

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
@quiet-node quiet-node changed the title Search feature v3: agentic pipeline with live traces feat: introduce agentic search pipeline with live trace streaming Apr 21, 2026
flushSync is intentionally used in the search trace handler to make
trace blocks feel live rather than batched at the next paint. Configure
the rule off at the config level instead of suppressing it with
per-line disable comments, which were fragile due to rule name format
differences between @eslint-react/eslint-plugin v3 and v4.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…ync violation

Tauri channel events arrive as separate macrotasks, so React 18 automatic
batching does not merge them across callbacks. flushSync is not needed
for live trace updates in this context. Removing it eliminates the lint
violation without any UX regression.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…ync violation

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
- Reorganize README to include search sandbox setup as Step 2\n- Refine SandboxSetupCard copy for better consumer clarity\n- Change Setup Guide link to use tauri::open_url for reliable browser opening\n- Update tests and restore 100% coverage

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
@quiet-node quiet-node merged commit 445534f into main Apr 22, 2026
3 checks passed
@quiet-node quiet-node deleted the feat/search-feature-v3 branch April 22, 2026 04:38
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