chore(agent): phase roadmap from TODO.md — 3 phases (138–140)
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
# Phase 139 — The Reading tool line shows which lines are being read
|
||||
|
||||
**Source:** `TODO.md` L4 — "The \"Reading\" tool UI output doesn't show what lines are being read, just the same file over and over which is confusing to the user"
|
||||
**Story:** n/a (TODO-derived phase, Protocol B append).
|
||||
**Context:** `app/api/chat.py` — the SSE `tool` frame (`ChatToolEvent`) ALREADY carries `offset` / `limit` for a range `read` (phase 131 task 04 — the fields land on the frame only when well-formed via `_whole_line_number` (L307) and are omitted otherwise, so non-range frames are byte-identical); the frame is emitted at ~L928-952. `frontend/assets/app.js` — `appendToolLine(wrap, name, argument)` (L1119 — the read branch builds `📄 Reading ` + a `<code>{argument}</code>` child; the line bytes are pinned by the phase-87 emoji guard + the phase-117 disclosure pins), the live `tool` branch (L3103-3137 — reads only `ev.name` / `ev.argument`; `toolAcc.push({ name, argument })` L3116; the status label `is reading ${argument}` L3125), `toolAcc` (L2993 — the per-turn accumulator persisted as the saved chat's `tools` array at L3229 / L3310 / L3361 / L3395), the restore path (~L2030-2045 — `m.tools` → `appendToolLine(wrap, t.name, arg)` + the phase-95 truncation marker). `frontend/assets/shared.js` — `addToolLines(wrap, tools)` (~L175+ — the shared page's parity copy of `appendToolLine` with the pinned-identical line literals). `tests/integration/test_api.py` — the frontend emoji guard strips precisely the pinned emoji literals from `app.js` / `shared.js` (no new emoji may be introduced). `tests/e2e/test_read_line_ranges.py` + the phase-131 mock flow (`read the lines` marker → grep → `read(offset, limit)`) in `tests/e2e/mock_llm.py`.
|
||||
|
||||
## Objective
|
||||
When the agent range-reads a document (`read(path, offset, limit)` — phase 131), the Reading tool line shows WHICH lines — `📄 Reading <code>source/path</code> (lines 42–71)` — so repeated reads of the same file are distinguishable, live, on restore, and on the shared page; a plain read renders byte-identical to today.
|
||||
|
||||
## Dependencies
|
||||
- `138_markdown_blockquotes` (todo) — pipeline order only; no code overlap (139 touches the tool-line rendering, 138 the markdown renderer).
|
||||
- Code: the phase-131 wire fields (complete) — nothing new server-side.
|
||||
|
||||
## Design (shared by all tasks — the executor reads this, not the chat)
|
||||
- **The suffix (task 01):** one new helper in `app.js` — `readRangeSuffix(offset, limit) → string | ""` — the single source of truth for the wording (used by both the tool line and the status label): both present → ` (lines {offset}–{offset + limit - 1})` (EN-DASH; the pinned copy — ASSUMPTION A1: the range shown is the model's REQUEST; the UI does not know the document length, so a `limit` past EOF renders the requested extent); offset only → ` (from line {offset})`; limit only → ` (first {limit} lines)`; neither / malformed → `""`. The JS well-formedness guard mirrors the server's `_whole_line_number`: `Number.isInteger(x) && x >= 1`.
|
||||
- **`appendToolLine` (task 01):** the signature becomes `appendToolLine(wrap, name, argument, offset = null, limit = null)` — the read arm (`read` / `read_document`) appends, AFTER the `<code>` child, a `<span class="read-range">` with `textContent = readRangeSuffix(offset, limit)` ONLY when the suffix is non-empty (a plain read's line stays byte-identical — the phase-87/117 pins + the emoji guard stay green; no new emoji). `shared.js`'s `addToolLines` applies the identical suffix to its pinned-identical line (the parity contract — the shared page re-renders saved records that now carry the fields).
|
||||
- **The live branch (task 02):** read `ev.offset` / `ev.limit` with the same guard (absent / malformed → `null`); `toolAcc.push({ name, argument, offset, limit })` with the fields included ONLY when non-null (a non-range record keeps the exact pre-phase `{name, argument}` shape — the `bor.chat.v1` save JSONB is additive; old records without the fields restore byte-identically, no migration); pass both to `appendToolLine`; the status label becomes `${brand()} is reading ${argument}${suffix}` (same helper — ASSUMPTION A2).
|
||||
- **The restore path (task 02):** `t.offset` / `t.limit` (same guard) → `appendToolLine(wrap, t.name, arg, tOffset, tLimit)`; the phase-95 truncation note is untouched (it keys on the `<code>` argument — verify with the existing pins).
|
||||
- **E2E (task 03):** ONE new mock flow following the phase-131 pattern (user-message marker, deterministic replies, module-header docstring entry): the model range-reads the SAME document twice with different `offset`/`limit` — the owner's exact "same file over and over" scenario.
|
||||
|
||||
## Tasks
|
||||
1. `01_tool_line_range_suffix.md` — `readRangeSuffix` + the `appendToolLine` / `addToolLines` suffix rendering.
|
||||
2. `02_live_restore_persist.md` — the live branch's `ev.offset`/`ev.limit`, the `toolAcc` persistence, the restore path, the status label.
|
||||
3. `03_unit_and_e2e.md` — unit pins + node behavior + the mock flow + the dedicated Playwright E2E.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit: `tests/unit/test_read_range_tool_lines.py` (new — the `test_frontend_tool_states.py` pattern): source pins (the suffix template literals, the `Number.isInteger` guard, the `ev.offset`/`ev.limit` read, the `toolAcc` push carrying the fields, the restore pass-through, the shared.js parity) + node behavior (`appendToolLine` with a range vs without — the plain line byte-identical; the three suffix shapes).
|
||||
- E2E: `tests/e2e/test_read_range_tool_lines.py` (new), in isolation.
|
||||
- Coverage: **>90%** on `app/` (no `app/` code changes — the suite staying green IS the gate).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] a range read renders `📄 Reading <code>path</code> (lines N–M)` live; two range reads of the same file show DIFFERENT ranges (the owner's confusion is gone)
|
||||
- [ ] a plain read's line is byte-identical to pre-phase (the phase-87/117 pins + the emoji guard pass unedited)
|
||||
- [ ] the suffix survives save → restore (the chat page + the shared page render it from the saved record)
|
||||
- [ ] `uv run pytest` green; coverage TOTAL >90%; `uv run pytest tests/e2e/test_read_range_tool_lines.py -v --no-cov` green in isolation; `uv run ruff check . && uv run pyright` clean
|
||||
- [ ] one `--no-gpg-sign` Conventional Commit; phase dir moved to `.agents/phases/complete/`
|
||||
|
||||
## Locked decisions
|
||||
- **A1 — the suffix shows the model's REQUESTED range (`offset`…`offset+limit−1`); the UI cannot know the actual slice extent (owner-confirmed 2026-09-30, roadmap confirmation).**
|
||||
- **A2 — the status label carries the same suffix (`Brain is reading source/path (lines 42–71)`) (owner-confirmed 2026-09-30, roadmap confirmation).**
|
||||
|
||||
## Commit
|
||||
```bash
|
||||
git add frontend/ tests/ .agents/phases/ && git commit --no-gpg-sign -m "feat(ui): show the read tool's line range in the Reading line"
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# Task 01 — The range suffix on the Reading line (chat + shared page)
|
||||
|
||||
**Phase:** `139_read_range_tool_lines` · **Source:** `TODO.md:4` — "The \"Reading\" tool UI output doesn't show what lines are being read, just the same file over and over which is confusing to the user"
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
`appendToolLine` (and the shared page's `addToolLines`) render the requested line range as a suffix on the Reading line whenever a well-formed `offset`/`limit` is passed — createElement + textContent only, plain reads byte-identical.
|
||||
|
||||
## Work
|
||||
1. `frontend/assets/app.js` — add a `readRangeSuffix(offset, limit)` helper next to `appendToolLine`: the well-formedness guard `Number.isInteger(x) && x >= 1` (mirrors the server's `_whole_line_number`); both present → ` (lines ${offset}–${offset + limit - 1})` (en-dash — the pinned copy); offset only → ` (from line ${offset})`; limit only → ` (first ${limit} lines)`; otherwise → `""`.
|
||||
2. `frontend/assets/app.js` — `appendToolLine(wrap, name, argument, offset = null, limit = null)`: in the read arm (`read` / `read_document`), compute `readRangeSuffix(offset, limit)`; when non-empty, append a `<span class="read-range">` (textContent = the suffix) AFTER the `<code>` child. No other arm changes; no new emoji (the guard in `tests/integration/test_api.py` strips precisely the pinned literals).
|
||||
3. `frontend/assets/shared.js` — `addToolLines`: the same suffix on the shared page's pinned-identical Reading line (the saved record carries the fields from task 02; a record without them renders exactly as today).
|
||||
- ASSUMPTION: the span is a SIBLING of the `<code>` child — the phase-95 truncation note is the precedent (it appends a span sibling and never rewrites the line's pinned template text).
|
||||
|
||||
## Testing & Quality
|
||||
- Unit: covered by task 03's file (source pins + node behavior).
|
||||
- Coverage: n/a (frontend).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] node repro: `appendToolLine` with `offset=42, limit=30` → the line's text is `📄 Reading <path> (lines 42–71)`; without the arguments → byte-identical to pre-task
|
||||
- [ ] `uv run pytest` green (the phase-87/117 pins + the emoji guard unedited); `uv run ruff check .` clean
|
||||
@@ -0,0 +1,22 @@
|
||||
# Task 02 — Wire the range through live frames, persistence, and restore
|
||||
|
||||
**Phase:** `139_read_range_tool_lines` · **Source:** `TODO.md:4` — "The \"Reading\" tool UI output doesn't show what lines are being read, just the same file over and over which is confusing to the user"
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
The `offset`/`limit` already on the SSE `tool` frame (phase 131) reach the Reading line live, persist in the saved chat's `tools` record (additively), and re-render on restore — chat page + shared page.
|
||||
|
||||
## Work
|
||||
1. `frontend/assets/app.js` — the live `tool` branch (~L3103): read `ev.offset` / `ev.limit` (the `Number.isInteger(x) && x >= 1` guard → `null` when absent / malformed); `toolAcc.push({ name, argument, offset, limit })` with the fields included ONLY when non-null (a non-range record keeps the exact pre-phase `{name, argument}` shape — the `bor.chat.v1` save JSONB is additive; old rows restore unchanged, no migration); pass both values to `appendToolLine`.
|
||||
2. `frontend/assets/app.js` — the status label (~L3125): `${brand()} is reading ${argument}${readRangeSuffix(offset, limit)}` (the grep / ls labels are untouched).
|
||||
3. `frontend/assets/app.js` — the restore path (~L2030): pass `t.offset` / `t.limit` (same guard) to `appendToolLine`; the phase-95 `appendTruncatedNote` call is untouched (it keys on the `<code>` argument).
|
||||
4. `frontend/assets/app.js` — the save-payload sites (L3229 / L3310 / L3361 / L3395) need no code change (they serialize `toolAcc` as-is) — verify the phase-83 save-payload-limit tests are unaffected by the two extra fields.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit: task 03's file (source pins for the `ev.offset`/`ev.limit` read, the `toolAcc` push shape, the restore pass-through).
|
||||
- Coverage: n/a (frontend).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] a live range read: the line shows the suffix AND the status label shows it AND `toolAcc` carries `{name, argument, offset, limit}`
|
||||
- [ ] a saved chat restored (the `/?chat=<id>` path) + the shared page re-render the suffix; a pre-phase saved record (no fields) renders byte-identical
|
||||
- [ ] `uv run pytest` green (the chat-persistence / save-payload suites unedited)
|
||||
@@ -0,0 +1,23 @@
|
||||
# Task 03 — Unit pins, node behavior, mock flow, dedicated E2E
|
||||
|
||||
**Phase:** `139_read_range_tool_lines` · **Source:** `TODO.md:4` — "The \"Reading\" tool UI output doesn't show what lines are being read, just the same file over and over which is confusing to the user"
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
Pin the suffix contract at the source level, add the owner's "same file over and over" scenario to the deterministic mock, and prove it end-to-end in a dedicated Playwright file, run in isolation.
|
||||
|
||||
## Work
|
||||
1. `tests/unit/test_read_range_tool_lines.py` (new) — the `test_frontend_tool_states.py` pattern: source pins (the ` (lines ${offset}–${offset + limit - 1})` / ` (from line …)` / ` (first … lines)` literals; the `Number.isInteger` guard; the live branch's `ev.offset` / `ev.limit` read; the `toolAcc` push carrying the fields; the restore pass-through; the shared.js identical literals) + node behavior (the three suffix shapes; the plain-read line byte-identical to pre-phase; the shared-page parity).
|
||||
2. `tests/e2e/mock_llm.py` — ONE new scripted flow (house style — a user-message marker, deterministic replies, a module-header docstring entry): the marker `read the same file twice` → round 1: a `read` tool call `(path, offset=10, limit=20)`; round 2: a `read` tool call `(path, offset=40, limit=15)`; round 3: the answer echoing both results (the phase-131 read-echo shape).
|
||||
3. `tests/e2e/test_read_range_tool_lines.py` (new) — dedicated, run in isolation (`--no-cov`, DB up), mock-only; a KB fixture with ONE multi-hundred-line document:
|
||||
- (a) the marker question → assert TWO Reading lines for the SAME `<code>` path carrying the distinct suffixes ` (lines 10–29)` and ` (lines 40–54)`;
|
||||
- (b) a plain-read flow (an existing mock flow with a `read` call and no range — e.g. the phase-131 plain-read case or the phase-37 flow) → assert the Reading line has NO `.read-range` span (byte-identical shape);
|
||||
- (c) save the chat → restore (reload `/?chat=<id>`) → assert the suffixes re-render from the saved record.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit: as above. E2E: as above. Coverage: **>90%** on `app/` (the gate; no `app/` changes).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/unit/test_read_range_tool_lines.py -v` green
|
||||
- [ ] `uv run pytest tests/e2e/test_read_range_tool_lines.py -v --no-cov` green in isolation
|
||||
- [ ] `uv run pytest` green, coverage TOTAL >90%, `uv run ruff check . && uv run pyright` clean
|
||||
Reference in New Issue
Block a user