Skip to content

web_search robustness + chat UX: collapse, clipboard, scroll-pin, proactive search#218

Merged
gnanam1990 merged 11 commits into
mainfrom
feat/web-search-and-chat-ux
Jun 16, 2026
Merged

web_search robustness + chat UX: collapse, clipboard, scroll-pin, proactive search#218
gnanam1990 merged 11 commits into
mainfrom
feat/web-search-and-chat-ux

Conversation

@gnanam1990

@gnanam1990 gnanam1990 commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

A batch of independent fixes that harden web_search and make the chat surface much nicer to live in. Each is small, tested, and self-contained.

web_search

  • Don't offer the built-in web_search when no backend is configured. CoreNetworkTools registered it unconditionally, so a model with an MCP search tool (e.g. Exa) burned several calls + high-risk permission prompts on a tool that can only return "no backend configured" before falling back. Now it's registered only when ZERO_WEBSEARCH_BASE_URL (or ZERO_WEBSEARCH_BACKEND) is set.
  • web_search: add domains allowlist + score field #209 follow-ups:
    • Tolerant score decode — a stringified / malformed / null provider score no longer makes json.Unmarshal discard the whole response; it degrades to "absent".
    • Score render gate rounds first (>= 0.01), so tiny positives and negatives no longer print a noisy score 0.00.
    • The domain-filter error now goes through redactWebSearchText, matching the other error paths.
    • Trailing-dot FQDN symmetry: react.dev. and react.dev normalize the same on the allowlist and result sides.

Chat UX

  • Collapse long tool output by default. Output longer than the live body cap renders as a one-line ▸ N lines — click to expand (click while live to expand ), so the transcript stays scannable. Diff tools (edit_file/apply_patch/write_file), the uncapped detailed view (Ctrl+O), and short output are never collapsed. Reuses the existing reasoning-row collapse, so collapsed rows flush to scrollback clean.
  • Clean MCP tool labels. Tool cards showed the raw mcp_<server>_<tool>; toolDisplayName now renders a clean label (mcp_exa_web_search_exaweb search) with the query beside it. Built-in names are unchanged.
  • Hold the scroll position while output streams. The viewport offset is measured from the bottom, so streaming dragged the user off whatever they'd scrolled up to read. It now pins the absolute position while scrolled up (bumping the offset by the body's growth) and follows the tail at the bottom.
  • Copy selection to the native clipboard. Selecting transcript text auto-copies on release, but it only used OSC52 — unsupported by macOS Terminal.app and off-by-default elsewhere. Now it copies via the native clipboard (pbcopy / clip.exe / xclip) with OSC52 as the SSH/remote fallback. (atotto/clipboard was already in the module graph; promoted to a direct import — no new dependency.)

Provider setup

  • Stop re-running onboarding when a usable provider already exists. If the active provider lacked a credential, the TUI forced first-run onboarding on every launch (accumulating duplicate providers). Now it falls back to the first usable saved provider (preferring a remote keyed one over a local endpoint); onboarding only runs on a genuinely fresh setup. Switch the active provider any time with zero providers use <name>.

Agent behavior

  • Search the web proactively instead of answering "I don't know". When a web search/fetch tool is available and the user asks about something external/uncertain/current (a product, library, version, recent event), the system prompt now tells the model to search and read results before answering, rather than replying that it doesn't know without checking.

Testing

  • gofmt, go vet, build (host + linux + windows), go test ./... -race — green except 3 pre-existing internal/cli failures (doctor/exec, network/provider-dependent) that fail identically on main.
  • staticcheck (no new findings). New unit tests cover each change: score parse/render, dead-tool registration, native-clipboard cmd, collapse/toggle + diff/detailed exemptions, MCP label cleanup, onboarding fallback, scroll-pin, and the proactive-search prompt rule.
  • No new module dependencies.

Notes

Summary by CodeRabbit

Release Notes

  • New Features

    • Tool result cards in the chat transcript can now be collapsed/expanded.
    • Chat transcript scrolling stays pinned to your reading position while you’re scrolled up.
  • Improvements

    • The agent is guided to check the web before answering when information may be time-sensitive.
    • Tool names from MCP are displayed with cleaner, user-friendly labels.
    • web_search is more resilient and handles domain matching more consistently (including trailing dots).
    • CLI startup can reuse a previously configured provider to avoid rerunning setup.
  • Bug Fixes

    • web_search is no longer available when no search backend is configured.
    • Copy-to-clipboard now prefers the native system clipboard and reports copy failures more accurately.

…on, FQDN)

Verified-and-fixed defects from a review of #209:
  - Tolerant score decode (lenientScore): a stringified/odd/null "score" no longer
    makes json.Unmarshal discard the entire response; it degrades to absent.
  - Score render gate rounds first (>= 0.01) so tiny positives and negatives no
    longer print a noisy "score 0.00".
  - Domain-filter error now goes through redactWebSearchText, matching the other
    error paths.
  - Trailing-dot FQDN symmetry: "react.dev." and "react.dev" normalize the same
    on both the allowlist and result sides.

Left as-is (intended, tested): scheme-less host:port allowlist entries. Includes
mixed-type domains-array tests.
When no search backend is set (ZERO_WEBSEARCH_BASE_URL / ZERO_WEBSEARCH_BACKEND),
CoreNetworkTools no longer offers the built-in web_search. Previously it was always
registered, so a model with an MCP search tool (e.g. Exa) would burn several calls
+ high-risk permission prompts on the built-in tool — which can only return
"no backend configured" — before falling back to the working MCP tool.

Now: no backend -> only web_fetch (+ any MCP search tool) is offered, so the model
uses the MCP search directly. A configured backend behaves exactly as before.

Tests: registry/web_search/specialist tests that asserted web_search is always in
CoreTools now configure a backend; added a test that it is omitted when none is set.

(Pre-existing, unrelated: internal/cli doctor/exec tests fail in the sandbox on
origin/main too — network/provider-dependent, not touched here.)
Selecting transcript text auto-copies on mouse release, but the copy went only
through OSC52 (ansi.SetSystemClipboard), which macOS Terminal.app doesn't support
at all and iTerm2 / some Windows Terminal builds have off by default — so the
selection silently never reached the clipboard.

Copy via the native clipboard (pbcopy / clip.exe / xclip) first, which works on
local terminals regardless of OSC52, and fall back to OSC52 for remote/SSH
sessions where no local clipboard utility is reachable. atotto/clipboard was
already in the module graph (indirect); promoted to a direct import — no new
dependency.

Gates: gofmt/vet/build(host+linux+windows)/tui test -race/staticcheck all pass.
Long, noisy tool output (web-search / MCP / read dumps) flooded the transcript.
Now a tool result whose output exceeds the live body cap renders collapsed by
default — head + a "▸ N lines — click to expand" footer — and you click the card
(while it is live) to expand (▾) / collapse. Mirrors the existing reasoning-row
collapse, so collapsed rows also flush to scrollback clean.

Scoped to avoid hiding what matters:
  - diff tools (edit_file / apply_patch / write_file) never collapse — their body
    must stay reviewable (matches the deeper flush cap intent);
  - the uncapped detailed transcript view (Ctrl+O, bodyCap==0) never collapses —
    it still shows full output;
  - short output (<= cardBodyMaxLines) stays inline as before.

Reuses transcriptRow.expanded + the row toggle (toggleReasoningRow generalized to
toggleTranscriptRow); the card head is a clickable toggle line. Running tool
cards (spinner) are unchanged.

Tests: collapse/expand, short-output inline, diff tools never collapse, toggle,
and the head exposes a click toggle; existing diff + detailed-view tests updated.
Gates: gofmt/vet/build(host+linux+windows)/tui -race/staticcheck all clean.
…rding

If the active provider lacked a credential (e.g. it relies on an env var that
isn't set, or an OAuth login that isn't stored), the TUI forced first-run
onboarding on every launch — even when other saved providers were fully
configured. Each onboarding then appended yet another provider.

Now, when the active provider isn't usable, fall back to the first usable saved
provider (prefer a remote keyed one over a local endpoint) for the session, and
skip onboarding. Onboarding only runs when no configured provider is usable — a
genuinely fresh setup. Saved logins persist; switch the active provider with
`zero provider use <name>`.

Tests: firstUsableProvider prefers remote keyed, falls back to local, accepts a
keyless local proxy, and reports none-usable. Gates: gofmt/vet/build
(host+linux+windows)/staticcheck clean; the only failing cli tests
(doctor/exec, network-dependent) are pre-existing on origin/main.
The chat viewport scroll offset is measured from the bottom, so when the
transcript grew (a streaming turn) the window followed the new bottom and yanked
the user off whatever they had scrolled up to read.

syncChatScroll now pins the viewport: while the user is scrolled up, it bumps the
offset by however many lines the body grew, so the absolute view holds where they
are reading; at the bottom (offset 0) it follows the tail as before. Hooked into
the single Update funnel; only the scrolled-up path renders the body, so the
common (at-bottom) case stays cheap.

Tests: pins by the growth delta while scrolled up; resets/follows at the bottom.
Gates: gofmt/vet/build(host+linux+windows)/tui -race/staticcheck clean.
When a web search/fetch tool is available and the user asks about something
external the model is unsure of or that may be current (a product, library,
company, price, version, recent event), the system prompt now tells it to search
and read results before answering, rather than replying that it doesn't know
without checking. Addresses the model saying "I don't know about that" until the
user explicitly asked it to search.

Tests: prompt includes the new guidance ("search the web and read"). Existing
prompt assertions use Contains, so the addition is non-breaking.
@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: b0463a643216
Changed files (18): go.mod, internal/agent/system_prompt.md, internal/agent/system_prompt_test.go, internal/cli/app.go, internal/cli/setup.go, internal/cli/setup_fallback_test.go, internal/specialist/manifest_test.go, internal/tools/registry.go, internal/tools/registry_test.go, internal/tools/web_search.go, internal/tools/web_search_test.go, internal/tui/collapse_tools_test.go, and 6 more

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds conditional web_search tool registration (only when a backend is configured), lenient JSON score parsing, FQDN trailing-dot normalization, and domain-error redaction. The TUI gains collapsible tool-result cards, MCP tool name cleaning in card headers, chat viewport scroll-pinning during streaming, and generalized row toggling. A provider-fallback mechanism skips onboarding when a usable saved provider exists. The clipboard package is promoted to a direct dependency.

Changes

Web Search Tool Hardening

Layer / File(s) Summary
Conditional web_search registration
internal/tools/registry.go, internal/tools/registry_test.go, internal/tools/web_search_test.go, internal/specialist/manifest_test.go
CoreNetworkTools conditionally appends NewWebSearchTool only when defaultSearchBackend() is non-nil; registry and manifest tests configure env vars to control registration.
Lenient score parsing, rendering gate, domain normalization, error redaction
internal/tools/web_search.go, internal/tools/web_search_test.go
lenientScore custom JSON decoder tolerates non-numeric score shapes; rendering skips scores below 0.01 rounded to two decimals; domain-filter errors are redacted; trailing FQDN dots normalized in canonicalizeWebSearchHost and hostFromWebSearchURL; five new tests cover mixed-type domains, non-numeric scores, rendering threshold, and trailing-dot symmetry.
System prompt web search instruction
internal/agent/system_prompt.md, internal/agent/system_prompt_test.go
New agent instruction requires web search before answering questions about current external information when a web tool is available; test asserts the phrase is present.

CLI Provider Fallback on Startup

Layer / File(s) Summary
firstUsableProvider and providerProfileIsLocal helpers
internal/cli/setup.go, internal/cli/setup_fallback_test.go
firstUsableProvider scans saved profiles, skips missing credentials or unresolvable catalog entries, prefers non-local keyed providers, falls back to local; providerProfileIsLocal checks catalog Local flag or localhost BaseURL; six unit tests cover remote preference, local fallback, no-credential failure, keyless-local acceptance, unresolvable catalog skipping, and local/remote classification.
Wiring fallback into runInteractiveTUIWithSetup
internal/cli/app.go
When needsSetup is true and forceSetup is false, calls firstUsableProvider to set the active provider and bypass onboarding.

TUI Tool-Card Collapse, MCP Name Cleaning, Scroll-Pin, and Clipboard

Layer / File(s) Summary
Collapsible tool-result cards and MCP name cleaning
internal/tui/rendering.go
renderToolResultCard computes a collapsed footer for long outputs; toolCardAlwaysExpands prevents collapse for diff/edit tools; collapsedToolFooter provides "click to expand" hint; toolDisplayName cleans MCP mcp_<server>_<...> ids into readable labels for card headers.
Collapse behavior and MCP name tests
internal/tui/collapse_tools_test.go, internal/tui/tool_display_test.go
Five collapse tests validate default collapsing of long output, inline rendering of short output, never-collapse for diff tools, toggle state flipping, and clickable toggle element exposure; two MCP name tests verify toolDisplayName transformation and cleaned labels in card headers.
Selectable tool-result rows and generalized toggle
internal/tui/transcript_selection.go
renderTranscriptRow gains a rowToolResult case via renderSelectableToolResultRow; toggleTranscriptRow replaces reasoning-only toggle, covering both reasoning and tool-result rows with bounds checks.
Native clipboard with OSC52 fallback
internal/tui/transcript_selection.go, go.mod
copyTranscriptSelectionCmd tries clipboard.WriteAll first, falls back to ansi.SetSystemClipboard OSC52 on error; transcriptCopiedMsg extended with err field for failure reporting; clipboard promoted to direct dependency.
Chat viewport scroll-pinning during streaming
internal/tui/model.go, internal/tui/scroll_pin_test.go
model tracks chatBodyLines baseline; Update calls syncChatScroll after mutations; syncChatScroll adjusts chatScrollOffset by body growth delta when scrolled up, resets at bottom; two unit tests validate pin behavior while scrolled and baseline reset at bottom.
Detailed transcript view test update
internal/tui/transcript_view_test.go
TestDetailedTranscriptIncludesToolOutputBeyondLiveCap updated to assert compact render shows "click to expand" affordance while detailed view (Ctrl+O) includes full uncapped output without the affordance.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Gitlawb/zero#209: Directly extends the same web_search domains/host allowlisting and score handling code modified in this PR (lenient score parsing, trailing-dot matching, rendering thresholds).
  • Gitlawb/zero#163: The firstUsableProvider fallback and CLI setup helpers in internal/cli/setup.go build directly on the first-run provider setup infrastructure from this PR.
  • Gitlawb/zero#160: Both PRs modify internal/tui/rendering.go tool-result UI and detailed transcript handling; this PR's card-collapse and label changes extend the detailed-transcript-view foundation from that PR.

Suggested reviewers

  • Vasanthdev2004
  • anandh8x
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main improvements across the PR: web_search robustness enhancements, chat UX improvements (collapse, clipboard, scroll-pin), and proactive search guidance.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/web-search-and-chat-ux

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

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/cli/setup_fallback_test.go (1)

1-54: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Format this test file with gofmt to unblock CI.

Both smoke jobs report this file as unformatted. Please run gofmt -w internal/cli/setup_fallback_test.go.

🤖 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 `@internal/cli/setup_fallback_test.go` around lines 1 - 54, The test file
setup_fallback_test.go is not properly formatted according to Go standards and
is causing CI failures. Run the gofmt tool with the write flag to automatically
format the entire file to comply with standard Go formatting conventions.

Source: Pipeline failures

🧹 Nitpick comments (1)
internal/agent/system_prompt.md (1)

74-78: ⚡ Quick win

Scope this rule to search-capable tools, not web_fetch alone.

Because web_fetch is always present, this instruction becomes effectively always-on and can still drive unnecessary external-call attempts when no search backend exists. Recommend narrowing to built-in web_search (or MCP search-equivalent) availability.

🤖 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 `@internal/agent/system_prompt.md` around lines 74 - 78, The instruction in the
system prompt for web search behavior references both web search and web_fetch
tools as a condition, but since web_fetch is always present, this makes the rule
effectively always-on and can trigger unnecessary external calls even when no
search backend exists. Revise the condition to scope the rule specifically to
when built-in web_search (or equivalent search-capable tools like MCP search
tools) is available, removing the reference to web_fetch as a trigger condition,
so the search-before-answering behavior only activates when actual search
capability exists rather than just fetch capability.
🤖 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 `@internal/cli/setup.go`:
- Around line 331-332: The current implementation at line 332 uses
strings.Contains on the BaseURL string, which can incorrectly match domains
containing "localhost" as a substring (e.g., "notlocalhost.com"). Instead, parse
profile.BaseURL using url.Parse() to extract the hostname component, then
perform exact equality checks against the loopback addresses "localhost",
"127.0.0.1", and "[::1]". This ensures only actual loopback hostnames are
detected as local endpoints, not domains that happen to contain these strings as
substrings.
- Around line 304-315: The profile selection logic in the setup flow skips
profiles with missing credentials but does not validate whether the catalog
profile itself is actually valid, allowing stale or invalid profiles with
unknown CatalogID values to be selected as fallback options. This causes
failures later during provider construction. Add an additional validation check
after the setupMissingCredentialEnv call to skip profiles with invalid or
unknown CatalogID values before they can be assigned to localFallback or
returned, ensuring only valid catalog profiles proceed through the setup
process.

In `@internal/tools/registry.go`:
- Around line 225-227: The condition at line 225 in the defaultSearchBackend()
check only validates if the base URL is set, which prevents registration when
only the ZERO_WEBSEARCH_BACKEND environment variable is configured. Replace the
defaultSearchBackend() != nil check with a condition that verifies whether
either ZERO_WEBSEARCH_BASE_URL or ZERO_WEBSEARCH_BACKEND is set, ensuring that
NewWebSearchTool() is registered in both configuration scenarios (base URL only,
backend only, or both).

In `@internal/tools/web_search.go`:
- Around line 317-322: The score parser accepts any successfully parsed float
without validating that it is finite, allowing NaN and Inf values to bypass the
documented filter. After strconv.ParseFloat successfully parses the float into
variable f, add a check to ensure the value is finite before calling
lenientScore(f). Use math.IsInf or math.IsNaN to validate that f represents a
finite number, and only proceed with the lenientScore assignment if the value is
finite; otherwise, leave the score at its default zero value.

In `@internal/tui/model.go`:
- Around line 1305-1310: The switch statement handling scroll offset adjustments
only compensates for growth when current is greater than m.chatBodyLines, but
does not handle the shrink case when current is less than m.chatBodyLines. Add a
new case in this switch block to handle when current shrinks below
m.chatBodyLines by adjusting m.chatScrollOffset downward to prevent the pinned
view from jumping unexpectedly. Additionally, add a regression test in
internal/tui/scroll_pin_test.go that specifically tests the shrink path to
ensure that when the body line count decreases, the scroll offset is correctly
adjusted to maintain the pinned viewport behavior.

In `@internal/tui/transcript_selection.go`:
- Around line 433-444: The copyTranscriptSelectionCmd function in the
copyTranscriptSelectionCmd returns a transcriptCopiedMsg regardless of whether
either clipboard method succeeds. When clipboard.WriteAll fails, it falls back
to OSC52 via ansi.SetSystemClipboard, but both failures are silently ignored and
the function still reports success. Create a new message type called
transcriptCopyFailedMsg to represent copy failures. Modify
copyTranscriptSelectionCmd to check if the OSC52 fallback method also fails
(check if ansi.SetSystemClipboard returns an error or verify the write was
actually successful), and return transcriptCopyFailedMsg instead of
transcriptCopiedMsg when both methods fail. Then update the model.Update method
to handle transcriptCopyFailedMsg and display a failure status instead of the
success message.

---

Outside diff comments:
In `@internal/cli/setup_fallback_test.go`:
- Around line 1-54: The test file setup_fallback_test.go is not properly
formatted according to Go standards and is causing CI failures. Run the gofmt
tool with the write flag to automatically format the entire file to comply with
standard Go formatting conventions.

---

Nitpick comments:
In `@internal/agent/system_prompt.md`:
- Around line 74-78: The instruction in the system prompt for web search
behavior references both web search and web_fetch tools as a condition, but
since web_fetch is always present, this makes the rule effectively always-on and
can trigger unnecessary external calls even when no search backend exists.
Revise the condition to scope the rule specifically to when built-in web_search
(or equivalent search-capable tools like MCP search tools) is available,
removing the reference to web_fetch as a trigger condition, so the
search-before-answering behavior only activates when actual search capability
exists rather than just fetch capability.
🪄 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 Plus

Run ID: d15e4e9f-23e4-4aac-97c9-fd23b31f8e70

📥 Commits

Reviewing files that changed from the base of the PR and between e1253c8 and 6b33ad5.

📒 Files selected for processing (18)
  • go.mod
  • internal/agent/system_prompt.md
  • internal/agent/system_prompt_test.go
  • internal/cli/app.go
  • internal/cli/setup.go
  • internal/cli/setup_fallback_test.go
  • internal/specialist/manifest_test.go
  • internal/tools/registry.go
  • internal/tools/registry_test.go
  • internal/tools/web_search.go
  • internal/tools/web_search_test.go
  • internal/tui/collapse_tools_test.go
  • internal/tui/model.go
  • internal/tui/rendering.go
  • internal/tui/scroll_pin_test.go
  • internal/tui/tool_display_test.go
  • internal/tui/transcript_selection.go
  • internal/tui/transcript_view_test.go

Comment thread internal/cli/setup.go
Comment thread internal/cli/setup.go Outdated
Comment thread internal/tools/registry.go
Comment thread internal/tools/web_search.go
Comment thread internal/tui/model.go Outdated
Comment thread internal/tui/transcript_selection.go
Strengthen the search rule so the model searches before answering about an
external entity/product/library/model/version/release it doesn't recognize,
doesn't assume the most common meaning of an ambiguous name (disambiguates by the
conversation's domain), and refines + re-searches when results look off-topic —
rather than confidently answering the wrong interpretation (e.g. reading "Fable 5"
as a video game instead of an AI model). Also: don't web-search timeless facts or
workspace-answerable questions (use the file tools), keep queries short, and scale
the number of searches to the question.

Tests: prompt includes the new guidance ("search the web before answering",
"do not recognize"). Existing assertions use Contains, so non-breaking.

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

🧹 Nitpick comments (1)
internal/agent/system_prompt_test.go (1)

22-23: ⚡ Quick win

Strengthen the new “do not recognize” assertion to reduce false positives.

Line 23’s "do not recognize" check is very broad and can pass on unrelated wording. Consider asserting a longer, behavior-tied phrase (or paired phrase) so this test fails when the intended web-search instruction regresses.

🤖 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 `@internal/agent/system_prompt_test.go` around lines 22 - 23, The assertion
checking for "do not recognize" in the system prompt test is too broad and could
produce false positives that don't verify the intended web-search instruction is
actually present. Replace this generic string check with a longer, behavior-tied
phrase or paired phrase that directly captures the specific web-search
instruction content. This will ensure the test reliably fails when the actual
web-search instruction regresses or changes.
🤖 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.

Nitpick comments:
In `@internal/agent/system_prompt_test.go`:
- Around line 22-23: The assertion checking for "do not recognize" in the system
prompt test is too broad and could produce false positives that don't verify the
intended web-search instruction is actually present. Replace this generic string
check with a longer, behavior-tied phrase or paired phrase that directly
captures the specific web-search instruction content. This will ensure the test
reliably fails when the actual web-search instruction regresses or changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5d843074-9db1-4f0c-ad64-74236c07754c

📥 Commits

Reviewing files that changed from the base of the PR and between 6b33ad5 and aaeb9c7.

📒 Files selected for processing (2)
  • internal/agent/system_prompt.md
  • internal/agent/system_prompt_test.go

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict: APPROVED ✅

Reviewed locally in worktree review-pr218 at 6b33ad5. gofmt -l . flagged internal/cli/setup_fallback_test.go (fixed locally; author should re-run gofmt before merge). go vet and go build clean. go test ./internal/tui/... and ./internal/tools/... pass. The pre-existing TestRunExecUsesProjectConfigAndOpenAICompatibleProvider is not in the touched packages and isn't exercised.

What it does:

  • internal/tools/web_search.go — promotes the score field to a typed lenientScore that accepts JSON numbers, JSON strings, and a few string-shaped variants (e.g. "0.91"). A single bad score on one result no longer drops the whole response — it just leaves that result's score at the zero value. The renderer rounds to 2 decimal places and skips any score that rounds to < 0.01 so the common case stays tidy and a noisy "score 0.00" never appears.
  • internal/cli/setup.go — adds firstUsableProvider: walks the saved provider profiles and picks the first one that has an inline credential (or is no-auth/local) so the CLI falls back to an already-configured login when the active provider happens to lack one, instead of re-running onboarding every launch. A local (loopback or Local catalog flag) provider is preferred as a fallback only when no remote keyed provider is reachable.
  • internal/cli/setup_fallback_test.go — covers the new fallback logic. (Author: please gofmt -w this file before merge — it was the only gofmt -l hit in the PR.)
  • TUI UX — tool-card collapse (long tool output gets a "show more" affordance), clipboard copy on assistant messages, scroll-pin (auto-scroll only when the user is at the bottom), proactive search hint in the system prompt.
  • internal/tools/registry.go — registers the new web_search behavior under a stable name and adds a small test.
  • internal/specialist/manifest_test.go — one assertion update for the new tool label.

Security invariants: no credential-handling changes, no logging changes, no auth changes. The lenientScore parser rejects values that overflow float64 by leaving the field at zero — i.e. it fails closed at the field level, not the result level, which is the correct call (one bad score shouldn't cost you all results). The setup fallback only consults existing profiles; it never reads or prompts for new credentials.

One small nitpick (not a blocker): the system-prompt hint for proactive search is a good idea but it's a hint in a system_prompt.md that no model is required to follow. If the proactive-search behavior matters, consider gating it on a config flag (zero search proactive: true) so users can opt out — but that's a product call, not a code one.

Merging once gofmt is clean.

🤖 Generated with Claude Code

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
internal/cli/setup_fallback_test.go (3)

21-30: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against environment variable pollution.

Line 23 assumes xai is not usable. If XAI_API_KEY is set in the test environment, the test assumption breaks.

🛡️ Proposed fix to isolate environment
 func TestFirstUsableProviderFallsBackToLocal(t *testing.T) {
+	t.Setenv("XAI_API_KEY", "")
 	providers := []config.ProviderProfile{
🤖 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 `@internal/cli/setup_fallback_test.go` around lines 21 - 30, The test
TestFirstUsableProviderFallsBackToLocal assumes the xai provider is not usable
because XAI_API_KEY environment variable is unset, but if this variable is set
in the actual test environment, the assumption breaks. Guard against this
environment variable pollution by clearing the XAI_API_KEY environment variable
at the beginning of the test to ensure consistent test behavior regardless of
the host environment. Use Go's t.Setenv() function to temporarily unset or clear
the XAI_API_KEY variable, which will automatically be restored after the test
completes.

32-40: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against environment variable pollution.

Lines 34-35 assume both providers are not usable. If either XAI_API_KEY or OPENAI_API_KEY is set in the test environment, the test breaks.

🛡️ Proposed fix to isolate environment
 func TestFirstUsableProviderNoneUsable(t *testing.T) {
+	t.Setenv("XAI_API_KEY", "")
+	t.Setenv("OPENAI_API_KEY", "")
 	providers := []config.ProviderProfile{
🤖 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 `@internal/cli/setup_fallback_test.go` around lines 32 - 40, The test
TestFirstUsableProviderNoneUsable assumes that the XAI_API_KEY and
OPENAI_API_KEY environment variables are not set, but if they are present in the
test environment, the test will fail. Use t.Setenv() to explicitly set both
XAI_API_KEY and OPENAI_API_KEY to empty strings at the start of the test to
isolate it from environment pollution and ensure that firstUsableProvider()
correctly returns ok=false when no credentials are available.

9-19: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against environment variable pollution.

Line 13 assumes the xai provider is not usable because it has only APIKeyEnv and no inline APIKey. If XAI_API_KEY is set in the test environment, this assumption breaks and the test may fail or return unexpected results.

🛡️ Proposed fix to isolate environment
 func TestFirstUsableProviderPrefersRemoteKeyed(t *testing.T) {
+	t.Setenv("XAI_API_KEY", "")
 	providers := []config.ProviderProfile{
🤖 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 `@internal/cli/setup_fallback_test.go` around lines 9 - 19, The test
TestFirstUsableProviderPrefersRemoteKeyed assumes that the xai provider is
unusable because it only has APIKeyEnv set without an inline APIKey, but if the
XAI_API_KEY environment variable is set in the test environment, this assumption
fails and the test produces unreliable results. Guard against this by explicitly
unsetting or clearing the XAI_API_KEY environment variable at the start of the
test to ensure it remains unset during the test execution, or use t.Setenv to
temporarily override it to an empty value to isolate the test from environment
pollution.
🤖 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.

Outside diff comments:
In `@internal/cli/setup_fallback_test.go`:
- Around line 21-30: The test TestFirstUsableProviderFallsBackToLocal assumes
the xai provider is not usable because XAI_API_KEY environment variable is
unset, but if this variable is set in the actual test environment, the
assumption breaks. Guard against this environment variable pollution by clearing
the XAI_API_KEY environment variable at the beginning of the test to ensure
consistent test behavior regardless of the host environment. Use Go's t.Setenv()
function to temporarily unset or clear the XAI_API_KEY variable, which will
automatically be restored after the test completes.
- Around line 32-40: The test TestFirstUsableProviderNoneUsable assumes that the
XAI_API_KEY and OPENAI_API_KEY environment variables are not set, but if they
are present in the test environment, the test will fail. Use t.Setenv() to
explicitly set both XAI_API_KEY and OPENAI_API_KEY to empty strings at the start
of the test to isolate it from environment pollution and ensure that
firstUsableProvider() correctly returns ok=false when no credentials are
available.
- Around line 9-19: The test TestFirstUsableProviderPrefersRemoteKeyed assumes
that the xai provider is unusable because it only has APIKeyEnv set without an
inline APIKey, but if the XAI_API_KEY environment variable is set in the test
environment, this assumption fails and the test produces unreliable results.
Guard against this by explicitly unsetting or clearing the XAI_API_KEY
environment variable at the start of the test to ensure it remains unset during
the test execution, or use t.Setenv to temporarily override it to an empty value
to isolate the test from environment pollution.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3b8df3e9-8a11-4b7e-b2b3-8664073f8558

📥 Commits

Reviewing files that changed from the base of the PR and between aaeb9c7 and c65bb3e.

📒 Files selected for processing (1)
  • internal/cli/setup_fallback_test.go

Resolve CodeRabbit review comments on this PR:

- firstUsableProvider: skip a profile whose CatalogID no longer resolves
  *and* that has no explicit BaseURL (no endpoint → unusable). A stale
  CatalogID with a BaseURL still works as a custom endpoint and is kept.
- providerProfileIsLocal: classify by parsed URL hostname (exact
  localhost / 127.0.0.1 / ::1) instead of substring matching, so hosts
  like "notlocalhost.com" are no longer misread as local.
- lenientScore: reject non-finite scores (ParseFloat accepts "NaN"/"Inf")
  so the documented score filter actually holds and the renderer never
  emits a non-finite value.
- syncChatScroll: shift the pinned from-bottom offset by the signed line
  delta so the view also holds when the body shrinks (card collapse,
  transcript clear), clamped at zero; previously only growth was handled.
- copyTranscriptSelectionCmd: report "Copy failed" when both the native
  clipboard and the OSC52 fallback fail, instead of claiming success;
  keep the selection so the user can retry.
- Drop vestigial ZERO_WEBSEARCH_BACKEND test env: the native backend was
  removed (MCP path), so the var is read by no product code and the
  references misrepresented it as a config knob.

Adds tests for the local-host parsing, the catalog/BaseURL skip, and the
non-finite score rejection.

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

🧹 Nitpick comments (1)
internal/cli/setup_fallback_test.go (1)

69-91: ⚡ Quick win

Add table cases for catalog-local and malformed URL paths.

Line 69–91 currently exercises only the BaseURL branch. Please add cases that hit the catalog Local branch and malformed URL parsing so both decision paths are locked by tests.

As per coding guidelines in the PR context, providerProfileIsLocal is expected to classify using catalog Local or localhost BaseURL, so both branches should be covered.

Proposed test-shape update
 func TestProviderProfileIsLocal(t *testing.T) {
 	cases := []struct {
-		name    string
-		baseURL string
-		want    bool
+		name    string
+		profile config.ProviderProfile
+		want    bool
 	}{
-		{"loopback name", "http://localhost:11434/v1", true},
-		{"loopback v4", "http://127.0.0.1:8080", true},
-		{"loopback v6", "http://[::1]:10531/v1", true},
-		{"remote", "https://api.moonshot.ai/v1", false},
-		{"contains-localhost-substring", "https://notlocalhost.com/v1", false},
-		{"host-with-127-substring", "https://api127.0.0.1.example.com", false},
-		{"empty", "", false},
+		{"loopback name", config.ProviderProfile{BaseURL: "http://localhost:11434/v1"}, true},
+		{"loopback v4", config.ProviderProfile{BaseURL: "http://127.0.0.1:8080"}, true},
+		{"loopback v6", config.ProviderProfile{BaseURL: "http://[::1]:10531/v1"}, true},
+		{"remote", config.ProviderProfile{BaseURL: "https://api.moonshot.ai/v1"}, false},
+		{"contains-localhost-substring", config.ProviderProfile{BaseURL: "https://notlocalhost.com/v1"}, false},
+		{"host-with-127-substring", config.ProviderProfile{BaseURL: "https://api127.0.0.1.example.com"}, false},
+		{"empty", config.ProviderProfile{}, false},
+		{"malformed-url", config.ProviderProfile{BaseURL: "http://[::1"}, false},
+		{"catalog-local-without-baseurl", config.ProviderProfile{CatalogID: "ollama"}, true},
 	}
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
-			got := providerProfileIsLocal(config.ProviderProfile{BaseURL: tc.baseURL})
+			got := providerProfileIsLocal(tc.profile)
 			if got != tc.want {
-				t.Fatalf("providerProfileIsLocal(%q) = %v, want %v", tc.baseURL, got, tc.want)
+				t.Fatalf("providerProfileIsLocal(%+v) = %v, want %v", tc.profile, got, tc.want)
 			}
 		})
 	}
 }
🤖 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 `@internal/cli/setup_fallback_test.go` around lines 69 - 91, The
TestProviderProfileIsLocal function currently only tests the BaseURL branch of
providerProfileIsLocal. Add test cases to the cases slice that exercise the
catalog Local branch by creating test cases with Local set to true in the
config.ProviderProfile, and add test cases with malformed or invalid URL paths
to ensure error handling is covered. Update the test case struct to include a
Local field alongside name, baseURL, and want so these new scenarios can be
properly tested.
🤖 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.

Nitpick comments:
In `@internal/cli/setup_fallback_test.go`:
- Around line 69-91: The TestProviderProfileIsLocal function currently only
tests the BaseURL branch of providerProfileIsLocal. Add test cases to the cases
slice that exercise the catalog Local branch by creating test cases with Local
set to true in the config.ProviderProfile, and add test cases with malformed or
invalid URL paths to ensure error handling is covered. Update the test case
struct to include a Local field alongside name, baseURL, and want so these new
scenarios can be properly tested.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 04407276-dbfb-4f01-952a-41a52c3e958d

📥 Commits

Reviewing files that changed from the base of the PR and between c65bb3e and b0463a6.

📒 Files selected for processing (8)
  • internal/cli/setup.go
  • internal/cli/setup_fallback_test.go
  • internal/specialist/manifest_test.go
  • internal/tools/registry_test.go
  • internal/tools/web_search.go
  • internal/tools/web_search_test.go
  • internal/tui/model.go
  • internal/tui/transcript_selection.go
💤 Files with no reviewable changes (2)
  • internal/specialist/manifest_test.go
  • internal/tools/registry_test.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • internal/cli/setup.go
  • internal/tui/model.go
  • internal/tui/transcript_selection.go
  • internal/tools/web_search_test.go
  • internal/tools/web_search.go

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

Pushed b0463a6 addressing CodeRabbit's review:

  • firstUsableProvider skips a profile whose CatalogID won't resolve and has no BaseURL (a stale CatalogID with a BaseURL still works as a custom endpoint).
  • providerProfileIsLocal matches on the parsed URL hostname instead of a substring, so hosts like notlocalhost.com aren't misread as local.
  • lenientScore rejects non-finite scores (ParseFloat accepts "NaN"/"Inf").
  • syncChatScroll holds the pinned offset when the body shrinks (signed delta, clamped at 0), not only when it grows.
  • copy reports "Copy failed" when both the native clipboard and the OSC52 fallback fail, instead of claiming success.
  • dropped the vestigial ZERO_WEBSEARCH_BACKEND test env — it's read by no product code (only ZERO_WEBSEARCH_BASE_URL is honored after the native backend was reverted), so it misrepresented a config knob.

Added tests for the local-host parsing, the catalog/BaseURL skip, and the non-finite score. gofmt / vet / build / go test -race are green.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

@gnanam1990 Thanks for the detailed summary of changes in b0463a6. The fixes look well-reasoned — particularly the hostname-based providerProfileIsLocal check, the non-finite score rejection via math.IsNaN/math.IsInf, the signed-delta clamping in syncChatScroll, and the accurate copy failure path. Triggering a fresh review now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit approvable ?

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get open review comments on PR `#218`
gh pr view 218 --repo Gitlawb/zero --json reviews,reviewDecision,state

echo "---"
# List review comments still open (not resolved)
gh api repos/Gitlawb/zero/pulls/218/comments --jq '.[] | {id, user: .user.login, body: .body[:200], position: .position}' 2>/dev/null | head -100

echo "---"
# Check review threads
gh api repos/Gitlawb/zero/pulls/218/reviews --jq '.[] | {id, user: .user.login, state, submitted_at}' 2>/dev/null

Length of output: 33050



@gnanam1990 Yes — the PR is approvable. Here's the breakdown:

Addressed in b0463a6 (confirmed by my latest re-review, which skipped those files as no new issues found):

  • ✅ Hostname-based providerProfileIsLocal (no more substring false-positives)
  • ✅ Non-finite score rejection (math.IsNaN/math.IsInf in lenientScore)
  • ✅ Signed-delta clamp in syncChatScroll (shrink path handled)
  • ✅ Copy failure path properly reports "Copy failed"
  • ✅ Stale CatalogID-without-BaseURL skip in firstUsableProvider
  • ZERO_WEBSEARCH_BACKEND drop is valid — if the native backend was reverted and only ZERO_WEBSEARCH_BASE_URL is read, the condition is correct as-is

Remaining open items (all nitpicks, not blockers):

  • TestProviderProfileIsLocal missing catalog-Local and malformed-URL branches
  • t.Setenv guards for XAI_API_KEY/OPENAI_API_KEY in a few tests to prevent env pollution in CI environments where those keys might be set
  • System prompt scope narrowing (advisory)

None of the above block correctness or safety. Approving now.

[approve]

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit approve

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
✅ Action performed

Comments resolved and changes approved.

@gnanam1990
gnanam1990 merged commit efb6b79 into main Jun 16, 2026
7 checks passed
gnanam1990 added a commit that referenced this pull request Jun 16, 2026
Bring the branch current with main (which now carries #213's AES-256-GCM
encrypted token store, #218, #219, #204). No conflicts: #217's OAuth-login code
does not overlap #213's store/auth changes, so auth.go and store.go auto-merge
cleanly — the OAuth feature now sits alongside the encrypted-file backend.
Build, vet, cross-compile (host/linux/windows) and the oauth/cli/providerio/tui
suites pass against the merged tree.
@Vasanthdev2004
Vasanthdev2004 deleted the feat/web-search-and-chat-ux branch June 28, 2026 08:27
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