phase: 146_search_tool_scope
Phase 146 final verification pass complete — all green, tree untouched by me.
**Verified (final pass — both tasks already in `complete/`):**
- Scoped grep streams `{"type":"tool","name":"grep","argument":<pattern>,"path":<source/path>}`; bare grep/read/ls frames dump byte-identical (unit matrix in `test_sse_events.py` + builder pins in `test_chat_gate.py` L1364–1448)
- Tool line `🔎 Searching for <pattern> in <source/path>` (two `<code>` spans, `.grep-scope` sibling, no new emoji); bare line byte-identical (unit Node test + E2E)
- Status label "… is searching for X in Y" scoped-only; `path` persisted additively on the saved tool record and re-rendered on `/?chat=<id>` restore; pre-phase records restore byte-identically (E2E tests 2 & 3)
**Results (exact commands):**
- `uv run pytest --cov=app --cov-report=term-missing` → **3419 passed**, `app/` coverage **99%** (>90% ✓)
- `uv run pytest tests/e2e/test_search_tool_scope.py -v --no-cov` → **3 passed** in isolation ✓
- `uv run ruff check . && uv run pyright` → clean (0 errors) ✓
**Completion criteria:** all 6 ✓ (wire shape, UI line, status label, save/restore, gates, no completed-phase behavior change — full suite green includes all prior-phase pins).
**Documentation:** README.md (incl. its phase-146 section) and AGENTS.md reviewed in full against the codebase — verified consistent, no changes needed (`.env.example` ⊆ README config tables; no stale vars; phase-100 72rem rule in AGENTS.md §5 matches shipped CSS; no-CDN/smoke-DB/gpgsign claims all confirmed).
**Notable:** none — no defects found, no live-infrastructure touched, nothing committed per harness protocol.
**Next pending phase:** `147_sync_changed_only`.
This commit is contained in:
+55
-5
@@ -1119,8 +1119,21 @@ function closeToolCalls(wrap) {
|
||||
* (phase 14) carry the old names, so both generations render — no
|
||||
* migration. The content marks (the read/search/list glyphs) stay the
|
||||
* exact tool-line template literals — the frontend emoji guard strips
|
||||
* precisely those in this file. */
|
||||
function appendToolLine(wrap, name, argument, offset = null, limit = null) {
|
||||
* precisely those in this file.
|
||||
*
|
||||
* Phase 146 (task 02): the grep arm gains the additive `scope` param
|
||||
* (default null — the frame's `path` field / the saved record's
|
||||
* `path`): a SCOPED grep's line shows WHERE the search runs — the
|
||||
* pinned Searching label + the pattern <code>, then " in " + the scope
|
||||
* in a second <code> — so repeated patterns with different scopes read
|
||||
* as distinct searches (the "looks like the LLM is repeating itself"
|
||||
* confusion, TODO.md L3). The scope is a
|
||||
* .grep-scope span SIBLING after the pattern <code> (the phase-139
|
||||
* read-range-suffix precedent), appended ONLY when it is a non-empty
|
||||
* string: a bare grep's line stays byte-identical to pre-phase (the
|
||||
* phase-70/87/117 pins and the frontend emoji guard stay green — no
|
||||
* new emoji). */
|
||||
function appendToolLine(wrap, name, argument, offset = null, limit = null, scope = null) {
|
||||
const body = wrap?.querySelector?.(".msg-body");
|
||||
if (!body) return;
|
||||
// Phase 117 (D2): the lines ride in a native <details> disclosure —
|
||||
@@ -1172,6 +1185,22 @@ function appendToolLine(wrap, name, argument, offset = null, limit = null) {
|
||||
const code = document.createElement("code");
|
||||
code.textContent = argument; // the pattern is data, never markup
|
||||
line.appendChild(code);
|
||||
// Phase 146 (task 02): the SCOPE of a scoped grep — the re-searched
|
||||
// document's combined source/path — appended as a SIBLING after the
|
||||
// pattern <code> (the phase-139 read-range-suffix precedent): a
|
||||
// .grep-scope span holding the " in " text node + a second <code>,
|
||||
// ONLY when the scope is a non-empty string. A bare grep's line
|
||||
// stays byte-identical to pre-phase (no span, no extra bytes);
|
||||
// the scope is data, never markup (no new emoji).
|
||||
if (typeof scope === "string" && scope) {
|
||||
const scopeSpan = document.createElement("span");
|
||||
scopeSpan.className = "grep-scope";
|
||||
scopeSpan.appendChild(document.createTextNode(" in "));
|
||||
const scopeCode = document.createElement("code");
|
||||
scopeCode.textContent = scope; // the scope is data, never markup
|
||||
scopeSpan.appendChild(scopeCode);
|
||||
line.appendChild(scopeSpan);
|
||||
}
|
||||
} else if (name === "ls" && argument) {
|
||||
line.textContent = "🔎 Listing documents in ";
|
||||
const code = document.createElement("code");
|
||||
@@ -2087,7 +2116,14 @@ function renderStoredMessage(m) {
|
||||
Number.isInteger(t.offset) && t.offset >= 1 ? t.offset : null;
|
||||
const tLimit =
|
||||
Number.isInteger(t.limit) && t.limit >= 1 ? t.limit : null;
|
||||
appendToolLine(wrap, t.name, arg, tOffset, tLimit);
|
||||
// Phase 146 (task 02): a saved SCOPED grep (the live toolAcc
|
||||
// stamp — the additive `path` key on the bor.chat.v1 record)
|
||||
// re-renders the SAME scope through the SAME helper: the same
|
||||
// string guard → a record saved before the phase (no `path`
|
||||
// key) renders byte-identically (the phase-95 truncation note
|
||||
// is untouched — it keys on the <code> argument, not the scope).
|
||||
const tPath = typeof t.path === "string" && t.path ? t.path : null;
|
||||
appendToolLine(wrap, t.name, arg, tOffset, tLimit, tPath);
|
||||
if (t.truncated && arg) {
|
||||
appendTruncatedNote(wrap, arg, Number(t.chars_shown) || 0, Number(t.chars_total) || 0);
|
||||
}
|
||||
@@ -3183,9 +3219,23 @@ async function runTurn(text, { reask = false, image = null } = {}) {
|
||||
Number.isInteger(ev.offset) && ev.offset >= 1 ? ev.offset : null;
|
||||
const frameLimit =
|
||||
Number.isInteger(ev.limit) && ev.limit >= 1 ? ev.limit : null;
|
||||
// Phase 146 (task 02): the SCOPE of a scoped grep — the
|
||||
// frame's additive `path` field (ABSENT on every non-scoped
|
||||
// frame, never null — the ChatToolEvent serializer drops it
|
||||
// when None). The SAME string guard as `argument` → null when
|
||||
// absent / malformed; the record gains `path` ONLY when
|
||||
// non-null (a bare grep keeps the exact pre-phase
|
||||
// {name, argument} shape — the bor.chat.v1 save JSONB is
|
||||
// additive, old records restore byte-identically, no
|
||||
// migration), and the line + the status label show WHERE the
|
||||
// search runs (the "looks like the LLM is repeating itself"
|
||||
// confusion, TODO.md L3).
|
||||
const framePath =
|
||||
typeof ev.path === "string" && ev.path ? ev.path : null;
|
||||
const toolRecord = { name, argument };
|
||||
if (frameOffset !== null) toolRecord.offset = frameOffset;
|
||||
if (frameLimit !== null) toolRecord.limit = frameLimit;
|
||||
if (framePath !== null) toolRecord.path = framePath;
|
||||
toolAcc.push(toolRecord);
|
||||
clearTurnTimeout(); // the stream is alive — a frame arrived
|
||||
if (!wrap) wrap = addMessage("brain", "");
|
||||
@@ -3201,7 +3251,7 @@ async function runTurn(text, { reask = false, image = null } = {}) {
|
||||
(name === "read" || name === "read_document") && argument
|
||||
? `${brand()} is reading ${argument}${readRangeSuffix(frameOffset, frameLimit)}`
|
||||
: (name === "grep" || name === "search_documents") && argument
|
||||
? `${brand()} is searching for ${argument}`
|
||||
? `${brand()} is searching for ${argument}${framePath ? ` in ${framePath}` : ""}`
|
||||
: name === "ls" && argument
|
||||
? `${brand()} is listing documents in ${argument}`
|
||||
: `${brand()} is listing documents`;
|
||||
@@ -3211,7 +3261,7 @@ async function runTurn(text, { reask = false, image = null } = {}) {
|
||||
.querySelector("#typing-indicator .bubble")
|
||||
?.setAttribute("aria-label", toolStatus);
|
||||
}
|
||||
appendToolLine(wrap, name, argument, frameOffset, frameLimit);
|
||||
appendToolLine(wrap, name, argument, frameOffset, frameLimit, framePath);
|
||||
// Phase 87 (TODO.md L5): arm this line's elapsed clock — the
|
||||
// baseline resets per line, so each line counts its OWN
|
||||
// frameless silence. This live branch is the ONLY arm call site
|
||||
|
||||
Reference in New Issue
Block a user