phase: 192_verbatim_inline_midturn
Phase 192 — final verification pass complete. - **Verified (all 4 tasks already complete):** additive `verbatim_block` SSE frame (`VerbatimBlockPiece` → `ChatVerbatimBlockEvent` → chat dispatch, after the round's `tool`/`tool_result`, before the next round); `hydrateVerbatimNow` matched-only mid-turn pass + app.js branch; `dropPlacedTwin` per-locator done/restore dedupe; the dedicated E2E suite. - **Root cause of the rejected attempt:** my own concurrent background `pytest` run deadlocked the harness validation run on the shared Postgres (`psycopg.errors.DeadlockDetected`). No code defect — re-ran every gate **exclusively**. - `uv run pytest -q --cov=app --cov-report=term-missing` → **exit 0**, `TOTAL 7181 16 99%` (>90%) - `uv run pytest tests/e2e/test_verbatim_inline_midturn.py --no-cov` → **6 passed** (isolated) - `uv run pytest tests/e2e/test_verbatim_show_text.py tests/e2e/test_chat_history.py --no-cov` → **18 passed** (`test_chat_history.py` byte-unedited; the phase-182 suite's "no eighth type" pin was tightened, not loosened, to seven + `verbatim_block` with frame-order asserts) - `uv run ruff check .` → All checks passed (exit 0) · `uv run pyright` → **0 errors** - **Completion criteria:** single frame per success + zero per refusal ✅ · `BOR_ANSWER_TEXT=0` wire byte-identical (integration spelled baseline + E2E App B) ✅ · fence hydrates in place / unfenced lands at the call moment, before `done` ✅ · append+fence → 1 panel, two ranges → 2 panels ✅ · restore 1 panel/locator, `done.answer_text` + record key + `verbatim_blocks=N` unchanged ✅ · suite green, coverage 99%, lint/types clean ✅ · no-show_text turn byte-identical ✅ · commit → left to the harness (no git index/history commands run). - **Docs:** README reviewed line-by-line (2399 lines) + AGENTS.md in full; mechanically verified all 86 `BOR_*` settings, every `.env.example` var, table defaults, internal anchors (0 broken), ports 8123/8901, tree depth 1–8, pagination 25/1–100, 17 palette colors, `0029`/`0030` migrations, and every cited test file. Fixed 1 inconsistency: the verbatim "Parity" bullet claimed the live turn/reload/shared page share "the same three globals" — now names the settle-path trio and states that `hydrateVerbatimNow` is the ONE chat-only addition (a shared page has no stream). - **Notable:** `run_agent`'s four repeated piece unions were collapsed into the ONE exported `AgentPiece` alias, so a future piece type is spelled in one place; `test_frontend_edit_message.py`'s `runTurn` byte-pin was regenerated, not weakened. - **Next pending phase:** `193_sync_remaining_count`.
This commit is contained in:
@@ -4640,6 +4640,30 @@ async function runTurn(text, { reask = false, image = null } = {}) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (ev.type === "verbatim_block") {
|
||||
// Phase 192 (task 02, A2): the show_text panel lands at the moment
|
||||
// the call succeeds — the fence it names hydrates IN PLACE; an
|
||||
// unfenced block lands at the answer position now, not at done.
|
||||
// The done pass re-runs the same functions and can never double this
|
||||
// panel: hydrateVerbatim is idempotent (only .verbatim-pending is
|
||||
// touched), it DROPS this appended twin when the answer fences the
|
||||
// same locator later (dropPlacedTwin, phase 192 A3), and
|
||||
// appendUnlocatedVerbatim skips an already-placed locator. NO
|
||||
// client-side answer_text gate: the server's gate is the only one
|
||||
// (a BOR_ANSWER_TEXT=0 turn sends no frame, so this branch never
|
||||
// fires — a second, client-side gate would only drift).
|
||||
// `wrap` always exists here (the round's `tool` frame created the
|
||||
// live bubble) but the optional chain keeps a frame that arrives
|
||||
// without one a silent no-op, like the `tool_result` branch.
|
||||
const n = hydrateVerbatimNow(
|
||||
wrap?.querySelector(".bubble"),
|
||||
ev,
|
||||
VERBATIM_LINK_OPTS
|
||||
);
|
||||
// The append is the ZERO-match path only: a fence already on screen
|
||||
// keeps its panel IN PLACE (the model's order), and only a block no
|
||||
// placeholder names lands at the answer position.
|
||||
if (n === 0) appendUnlocatedVerbatim(wrap, [ev], VERBATIM_LINK_OPTS);
|
||||
} else if (ev.type === "error") {
|
||||
// Phase 182 (task 07): an `error` frame is terminal — the `done`
|
||||
// hydrate will never run, so a placeholder that streamed in stays at
|
||||
|
||||
+110
-11
@@ -6,11 +6,13 @@
|
||||
* locator, and the SERVER (`app.rag.verbatim.resolve_answer_blocks`,
|
||||
* task 05) puts the real bytes on the `done` frame as `answer_text`. This
|
||||
* file is the other half of that handshake: it RESOLVES the placeholders
|
||||
* against the frame's blocks (`hydrateVerbatim`), places the blocks the
|
||||
* answer never located (`appendUnlocatedVerbatim`), settles leftovers when
|
||||
* a turn ends without a `done` (`settleVerbatimPlaceholders`), gives every
|
||||
* settled answer code block a Copy button (`enhanceAnswerCodeBlocks`) and
|
||||
* owns the ONE clipboard path (`copyTextWithFallback`).
|
||||
* against the frame's blocks (`hydrateVerbatim`), settles the ONE block a
|
||||
* mid-turn `verbatim_block` frame carries (`hydrateVerbatimNow`, phase 192),
|
||||
* places the blocks the answer never located (`appendUnlocatedVerbatim`),
|
||||
* settles leftovers when a turn ends without a `done`
|
||||
* (`settleVerbatimPlaceholders`), gives every settled answer code block a
|
||||
* Copy button (`enhanceAnswerCodeBlocks`) and owns the ONE clipboard path
|
||||
* (`copyTextWithFallback`).
|
||||
*
|
||||
* WHY IT IS A CLASSIC SCRIPT, NOT A MODULE (the `markdown.js` pattern):
|
||||
* `index.html` and `shared.html` load it as a plain `<script>` right after
|
||||
@@ -38,7 +40,13 @@
|
||||
* whose range the model got wrong shows what the server actually read
|
||||
* (A2 made visible);
|
||||
* • a locator the server could not resolve says so — the unresolved node
|
||||
* carries NO badge, NO body and NONE of the model's bytes.
|
||||
* carries NO badge, NO body and NONE of the model's bytes;
|
||||
*
|
||||
* ONE CANONICAL LOCATOR, ONE PANEL (phase 192): the mid-turn frame can place
|
||||
* a block BEFORE the answer fences it, and hydration drops that appended
|
||||
* twin when the fence resolves (`dropPlacedTwin`) — the fence is where the
|
||||
* model wanted the bytes. The scan is PER LOCATOR, so two honest ranges of
|
||||
* one file stay two panels.
|
||||
*
|
||||
* The house frontend rules hold throughout: every node is built with
|
||||
* `createElement` + `textContent` (no HTML string anywhere — the data here
|
||||
@@ -412,6 +420,50 @@ function matchVerbatimBlock(blocks, key) {
|
||||
return samePath.length === 1 ? samePath[0] : null;
|
||||
}
|
||||
|
||||
/* The key a PLACED panel carries for a block — `buildVerbatimPanel`'s own
|
||||
* convention (the server's canonical locator, the `source/path` identity
|
||||
* when the block carries none). The ONE place that key is derived, so the
|
||||
* append-side placed-scan and the hydration-side twin-scan can never drift
|
||||
* (phase 192: the two are the same dedupe seen from either end). */
|
||||
function verbatimPlacedKey(block) {
|
||||
return typeof block.locator === "string" && block.locator
|
||||
? block.locator
|
||||
: verbatimIdentity(block.source, block.path);
|
||||
}
|
||||
|
||||
/* dropPlacedTwin(host, block, keep) — the phase-192 (A3) dedupe,
|
||||
* the twin a MID-TURN frame left behind.
|
||||
*
|
||||
* `hydrateVerbatimNow` appends a block no fence named YET (the call moment —
|
||||
* the model may fence it in a later round). When that fence finally hydrates,
|
||||
* TWO panels would carry the same canonical locator. The IN-TEXT panel wins:
|
||||
* the fence is where the model wanted the bytes, the appended panel already
|
||||
* served its mid-turn purpose. So the placed twin is dropped as its
|
||||
* placeholder resolves.
|
||||
*
|
||||
* PER LOCATOR, never per identity: two different ranges of one file are two
|
||||
* honest panels (`shown_texts` dedupes per canonical locator too), and a
|
||||
* pending or unresolved node is NOT a placement (the same exclusion
|
||||
* `appendUnlocatedVerbatim` uses). `keep` is the placeholder about to
|
||||
* hydrate — never the node this scan removes.
|
||||
*
|
||||
* A fence the model repeated for the SAME locator collapses the same way: the
|
||||
* answer shows those bytes once, at the last fence, never twice. */
|
||||
function dropPlacedTwin(host, block, keep) {
|
||||
if (!host || typeof host.querySelectorAll !== "function") return 0;
|
||||
const placedKey = verbatimPlacedKey(block);
|
||||
let dropped = 0;
|
||||
host.querySelectorAll(".verbatim[data-bor-locator]").forEach((el) => {
|
||||
if (el === keep) return;
|
||||
if (el.classList.contains("verbatim-pending")) return;
|
||||
if (el.classList.contains("verbatim-unresolved")) return;
|
||||
if (el.getAttribute("data-bor-locator") !== placedKey) return;
|
||||
el.remove();
|
||||
dropped += 1;
|
||||
});
|
||||
return dropped;
|
||||
}
|
||||
|
||||
/* hydrateVerbatim(root, blocks, opts) — replace every PENDING placeholder
|
||||
* under `root` with its panel, or with the unresolved state.
|
||||
*
|
||||
@@ -419,14 +471,26 @@ function matchVerbatimBlock(blocks, key) {
|
||||
* this function built (or one restored from storage) is skipped on a re-run,
|
||||
* which keeps a `Copied` state and a scrolled body alive. Never throws —
|
||||
* a missing/malformed `blocks` settles every placeholder to the honest
|
||||
* state instead. Returns the number of placeholders settled. */
|
||||
* state instead. Returns the number of placeholders settled.
|
||||
*
|
||||
* A MATCHED placeholder also drops the panel the mid-turn pass appended for
|
||||
* the same canonical locator (dropPlacedTwin, phase 192 A3) — one locator,
|
||||
* one panel, and it sits where the model fenced it. */
|
||||
function hydrateVerbatim(root, blocks, opts) {
|
||||
if (!root || typeof root.querySelectorAll !== "function") return 0;
|
||||
const list = verbatimBlocks(blocks);
|
||||
// The message wrap is the scan's bound: the mid-turn panel lives in
|
||||
// `.msg-body` beside the bubble, so a bubble-local walk would miss it.
|
||||
// With no `.msg` ancestor (a bare bubble, a fragment not mounted yet) the
|
||||
// ROOT bounds the scan — a missing wrap must not switch the dedupe off.
|
||||
const host =
|
||||
(typeof root.closest === "function" ? root.closest(".msg") : null) || root;
|
||||
let settled = 0;
|
||||
// The NodeList is STATIC, so replacing a node never disturbs the walk.
|
||||
root.querySelectorAll(".verbatim-pending[data-bor-locator]").forEach((node) => {
|
||||
const key = node.getAttribute("data-bor-locator") || "";
|
||||
const block = matchVerbatimBlock(list, key);
|
||||
if (block) dropPlacedTwin(host, block, node);
|
||||
node.replaceWith(
|
||||
block ? buildVerbatimPanel(block, opts) : buildVerbatimUnresolved(key, opts)
|
||||
);
|
||||
@@ -435,6 +499,44 @@ function hydrateVerbatim(root, blocks, opts) {
|
||||
return settled;
|
||||
}
|
||||
|
||||
/* hydrateVerbatimNow(root, block, opts) — the MID-TURN pass (phase 192,
|
||||
* A2): settle the PENDING placeholders THIS ONE block matches, leave
|
||||
* every other placeholder pending.
|
||||
*
|
||||
* The matching is the done path's semantics (matchVerbatimBlock with a
|
||||
* ONE-block list): the exact canonical locator, or the identity
|
||||
* fallback when the block is the only one of that identity — a fence
|
||||
* for range B of a shown file A hydrates with A's server extent (the
|
||||
* caption shows the SERVER's range, the phase-182 contract), so the
|
||||
* mid-turn pass and the done pass can never disagree.
|
||||
*
|
||||
* ONLY matched nodes are replaced (buildVerbatimPanel); the unmatched
|
||||
* pending nodes STAY .verbatim-pending — settling them is the
|
||||
* done/failure path's job (settleVerbatimPlaceholders), and a mid-turn
|
||||
* pass must never burn a placeholder for a not-yet-shown fence.
|
||||
*
|
||||
* Returns the number of placeholders settled (the caller appends the
|
||||
* block — appendUnlocatedVerbatim — only when it is 0). Never throws:
|
||||
* a missing root → 0, and a frame that is not a block settles nothing
|
||||
* (the house never-throw contract — the guard is `isVerbatimBlock`, the
|
||||
* done path's own). */
|
||||
function hydrateVerbatimNow(root, block, opts) {
|
||||
if (!root || typeof root.querySelectorAll !== "function") return 0;
|
||||
// A malformed frame settles NOTHING (not even to the honest state — the
|
||||
// `done` pass and the terminal settle still own this turn's placeholders).
|
||||
if (!isVerbatimBlock(block)) return 0;
|
||||
const list = [block];
|
||||
let settled = 0;
|
||||
root.querySelectorAll(".verbatim-pending[data-bor-locator]").forEach((node) => {
|
||||
const key = node.getAttribute("data-bor-locator") || "";
|
||||
const matched = matchVerbatimBlock(list, key);
|
||||
if (!matched) return; // STAYS pending — the done frame may resolve it
|
||||
node.replaceWith(buildVerbatimPanel(matched, opts));
|
||||
settled += 1;
|
||||
});
|
||||
return settled;
|
||||
}
|
||||
|
||||
/* appendUnlocatedVerbatim(wrap, blocks, opts) — the blocks NO placeholder
|
||||
* matched (a `show_text` call whose answer never located the file), appended
|
||||
* in the frame's order at the `answer_image` position: below the answer
|
||||
@@ -458,10 +560,7 @@ function appendUnlocatedVerbatim(wrap, blocks, opts) {
|
||||
});
|
||||
let appended = 0;
|
||||
for (const block of list) {
|
||||
const key =
|
||||
typeof block.locator === "string" && block.locator
|
||||
? block.locator
|
||||
: verbatimIdentity(block.source, block.path);
|
||||
const key = verbatimPlacedKey(block);
|
||||
if (placed.has(key)) continue;
|
||||
host.appendChild(buildVerbatimPanel(block, opts));
|
||||
placed.add(key);
|
||||
|
||||
Reference in New Issue
Block a user