chore(agent): phase roadmap from TODO.md (phases 73-75), clear the file
Convert the two unchecked TODO items into executable phases (Protocol B, appended after the 72 completed phases): - 73_hidden_tab_stream (TODO L3): a merely-hidden browser tab must never stop a generating answer; repro/root-cause decision tree + the pagehide partial-correlation fix + the hidden-tab E2E pin. - 74_llm_chat_history (TODO L4, history): client-provided history in POST /api/chat (stateless, A10) mapped through both the deflected and grounded agent paths, prior thinking blocks preserved via reasoning_content, capped oldest-first; mock echo marker + E2E. - 75_save_doc_full_session (TODO L4, save-as-doc): the Save-as-doc draft body becomes the full session transcript; edit-out happens in the existing doc-edit body; multi-turn git-verified E2E. Owner-confirmed assumptions A1-A7 are recorded as ASSUMPTION lines in the task files. TODO.md is cleared (items now live in .agents/phases/todo/).
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
# Phase 75 — "Save as doc" captures the whole chat session
|
||||
|
||||
**Source:** `TODO.md` L4 — "Chat history isn't being passed to the LLM. When the LLM responds and you ask a follow-up question the previous question/answer isn't passed to the model. Since my models support preserve thinking, make sure to pass previous thinking blocks as well. Then, update the \"save as doc\" process to include the output from the entire chat session rather than the last response. The user can edit out anything they don't want to keep from previous replies." (this phase covers the last two sentences; the history sentences are phase 74)
|
||||
**Story:** n/a (TODO-derived)
|
||||
**Context:** `frontend/assets/app.js` (`appendSaveAsDocButton(wrap, markdown)` ~L591, `saveAsDoc(btn, markdown)` ~L615 — today the body is ONLY the clicked bubble's raw answer; `defaultDocTitle()` ~L562; the `conversation` array is the full `bor.chat.v1` record), `app/api/doc_drafts.py` (draft create/edit/push — unchanged by this phase), `frontend/doc-edit.html` + `frontend/assets/doc-edit.js` (the existing edit screen: title / path / free-form editable body — the user's means of editing turns out), phase-59 E2E `tests/e2e/test_response_to_docs.py` (bare-repo fixture + admin flow + the single-turn body expectation that must move to the transcript shape), the mock's default composed answer (embeds the question's first 80 chars → different questions give byte-distinct deterministic answers).
|
||||
|
||||
## Objective
|
||||
"Save as doc" drafts a document from the ENTIRE chat session — every question and answer up to the click — instead of only the last response; the existing doc-edit screen's free-form body editing is how the user edits out anything they don't want to keep from previous replies (no new UI surface).
|
||||
|
||||
## Dependencies
|
||||
- `74_llm_chat_history` (todo) — the chat turn path (including its E2E pins) is settled first; this phase only touches the save-as-doc side of the same page.
|
||||
|
||||
## Tasks
|
||||
1. `01_session_transcript.md` — `buildSessionTranscript()` in `app.js`; `saveAsDoc` posts the transcript as the draft body; the phase-59 single-turn E2E expectation moves to the transcript shape.
|
||||
2. `02_e2e_full_session_save.md` — multi-turn session → save → all turns present in order → edit a turn out in doc-edit → push → bare-repo git verification; regressions + commit.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit/integration: frontend-only change — no `app/` code (the `doc_drafts` API is untouched; the >90% `app/` floor holds by the full suite).
|
||||
- E2E: new story suite `tests/e2e/test_save_doc_session.py` (run in isolation) + the updated expectation in the existing `tests/e2e/test_response_to_docs.py`, both on the bare-repo fixture (git as source of truth).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] The draft body for an N-turn session contains every turn in order (`## N. <question>` + the answer's raw markdown); the doc-edit screen shows the full transcript prefilled; editing a section out and pushing lands exactly the edited body in `BOR_DOCS_REPO` (verified with git, not UI text).
|
||||
- [ ] The single-turn behavior of phase 59 is preserved in transcript shape (updated E2E green).
|
||||
- [ ] `uv run pytest` green; coverage >90%; ruff + pyright clean.
|
||||
- [ ] One atomic `--no-gpg-sign` commit (e.g. `feat(docs): save the whole chat session as a doc`); phase dir moved to `.agents/phases/complete/`.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Task 01 — `saveAsDoc` posts a full-session transcript; single-turn E2E expectation updated
|
||||
|
||||
**Phase:** `75_save_doc_full_session` · **Source:** `TODO.md:4` — "Then, update the \"save as doc\" process to include the output from the entire chat session rather than the last response. The user can edit out anything they don't want to keep from previous replies."
|
||||
**Story:** n/a (TODO-derived)
|
||||
|
||||
## Objective
|
||||
The "Save as doc" draft's body becomes the whole conversation — every user question and brain answer up to the click, in order — instead of only the clicked bubble's answer; the user then edits out unwanted turns in the existing doc-edit body field before pushing.
|
||||
|
||||
## Work
|
||||
1. `frontend/assets/app.js` — add `buildSessionTranscript()` next to `defaultDocTitle()`/`saveAsDoc()` (~L562–L615):
|
||||
- Walks the `conversation` record in order. Each `who === "user"` entry opens a section; each following brain entry (up to the next user entry) is appended under it:
|
||||
```markdown
|
||||
## 1. <user question, raw text>
|
||||
|
||||
<brain answer, raw text>
|
||||
|
||||
## 2. <user question, raw text>
|
||||
|
||||
<brain answer, raw text>
|
||||
```
|
||||
- Numbering: 1-based per USER turn (a section per question; normally one answer per section).
|
||||
- Trailing whitespace collapsed to a single final newline.
|
||||
- - ASSUMPTION A6 (owner-confirmed 2026-09-08): the transcript format is the numbered `## N. <question>` + raw answer markdown shown above; ALL turns at click time are included (even when the button is on an earlier bubble); NO thinking blocks, NO source chips, NO tune metadata travel into the document; the default TITLE is unchanged (`defaultDocTitle()` — last user question, ≤120 chars).
|
||||
- - ASSUMPTION A7 (owner-confirmed 2026-09-08): "edit out anything they don't want to keep" = free-form editing in the EXISTING doc-edit body field (`frontend/doc-edit.html` / `frontend/assets/doc-edit.js` already expose the body as an editable textarea) — no new per-turn selection UI in this phase.
|
||||
- Stopped/partial brain turns appear as-is (their `text` is what the user saw); the user edits them out if unwanted (A7).
|
||||
2. `frontend/assets/app.js` — `saveAsDoc`: the draft's `body` becomes `buildSessionTranscript()` instead of the bubble's `markdown` (the POST to `/api/doc-drafts` at ~L623 otherwise unchanged: same `title` from `defaultDocTitle()`, same `docs/<slug>.md` path, same 201→`/doc-edit.html?draft=<token>` hand-off, same double-click guard). The `markdown` parameter of `saveAsDoc` is then unused — drop the parameter and update its single call site (`appendSaveAsDocButton`'s click binding ~L607); keep `appendSaveAsDocButton(wrap, markdown)`'s signature (the button is still appended per bubble).
|
||||
3. `tests/e2e/test_response_to_docs.py` — `test_save_edit_push`'s body expectation: today "body == the rendered answer's markdown source" → the SINGLE-turn transcript `## 1. <the question>\n\n<the mock answer>` (byte-stable: the mock's default composed answer embeds the question's first 80 chars). Everything else in that suite (push → git verification, guest/unconfigured pins) is unchanged.
|
||||
4. No backend change: `app/api/doc_drafts.py` accepts any body string; `frontend/doc-edit.html` renders it into the editable body as before.
|
||||
|
||||
## Testing & Quality
|
||||
- No `app/` code — coverage floor unaffected; the wire-level proof is the E2E (this task updates the existing phase-59 suite; task 02 adds the multi-turn suite).
|
||||
- Manual smoke: 2-turn conversation → Save as doc → the edit screen's body shows both Q/A sections before any editing.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `buildSessionTranscript()` produces the exact shape above for multi-turn records (section per user turn, raw texts, single trailing newline).
|
||||
- [ ] `saveAsDoc` posts the transcript; the parameter cleanup is done (no dead `markdown` argument); the double-click guard and hand-off are unchanged.
|
||||
- [ ] `tests/e2e/test_response_to_docs.py` (updated) green in isolation; `uv run pytest` green; ruff + pyright clean.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Task 02 — E2E: multi-turn session → save → all turns → edit one out → push (git-verified) + regressions + commit
|
||||
|
||||
**Phase:** `75_save_doc_full_session` · **Source:** `TODO.md:4` — "Then, update the \"save as doc\" process to include the output from the entire chat session rather than the last response. The user can edit out anything they don't want to keep from previous replies."
|
||||
**Story:** n/a (TODO-derived)
|
||||
|
||||
## Objective
|
||||
Prove the whole loop in the browser against the bare repo (git as source of truth, the phase-59 convention): the saved draft carries the ENTIRE session, and the user can edit an unwanted previous reply out before the document lands in the docs repo.
|
||||
|
||||
## Work
|
||||
1. `tests/e2e/test_save_doc_session.py` (Playwright; reuse the bare-repo fixture pattern from `tests/e2e/test_response_to_docs.py`: `git init --bare` + seeded work clone on `main`, app env `BOR_DOCS_REPO`/`BOR_DOCS_BRANCH=bor-docs`/`BOR_DOCS_BASE_BRANCH=main`/`BOR_DOCS_WORK_DIR`, admin login via `tests/e2e/auth_helpers.py`; KB fixture from `tests/e2e/test_chat_rag.py`):
|
||||
- `test_full_session_save_and_edit_out` —
|
||||
a. Three DISTINCT on-topic questions in one session (the mock's default composed answer embeds each question's first 80 chars, so the three answers are byte-distinct and assertable); wait for `done` after each.
|
||||
b. Click "Save as doc" on the LAST brain bubble → `/doc-edit.html?draft=<uuid>`; assert the prefilled body contains `## 1.`, `## 2.`, `## 3.` IN ORDER, each followed by that turn's answer text (byte-exact against the mock), and that the title is the last question (whitespace-collapsed ≤120 chars — the unchanged `defaultDocTitle()`); assert NO thinking scratchpad text leaked into the body (turn 1 asked with `think out loud`, so the record carries a `thinking` block the transcript must exclude).
|
||||
c. Edit out a previous reply: delete the entire section-2 block (heading + answer) from the body textarea; Push.
|
||||
d. Assert the status live region shows branch + sha, then GIT-VERIFY: `git -C <bare> show bor-docs:<path>` equals the EDITED body exactly — sections 1 and 3 present, section 2 (both question and answer) gone; the UI's sha prefix equals `git rev-parse bor-docs` (7 chars).
|
||||
- `test_earlier_bubble_button_saves_whole_session` — click "Save as doc" on the FIRST brain bubble after the third turn has completed → the draft body still contains all three sections (ASSUMPTION A6: the transcript is the whole session at click time, regardless of which bubble's button was used); cancel out without pushing (no git change: `bor-docs` still has the previous commit).
|
||||
2. Regression runs (isolation, AGENTS.md rule 9): `test_response_to_docs.py` (after task 01's expectation update — the phase-59 contract survives in transcript shape), `test_chat_rag.py` (the meta-row button on every bubble), `test_cache_busting.py` (page list unchanged — no new page in this phase).
|
||||
3. `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` >90%; `uv run ruff check . && uv run pyright` clean.
|
||||
4. Commit (Conventional Commits, `--no-gpg-sign`) — e.g. `feat(docs): save the whole chat session as a doc`; move `.agents/phases/todo/75_save_doc_full_session/` → `.agents/phases/complete/`.
|
||||
|
||||
## Testing & Quality
|
||||
- E2E: `uv run pytest tests/e2e/test_save_doc_session.py -v --no-cov` green in isolation (DB up, `git` on PATH — the phase-59 `pytest.skip` guard on absence).
|
||||
- Coverage: **>90%** on `app/` (no `app/` changes in this phase).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] The full-session → edit-out → push loop passes with the bare repo's file content as the assertion (section 2 provably absent from the pushed file; sections 1 and 3 byte-exact).
|
||||
- [ ] The earlier-bubble button saves the whole session (A6 pinned); the thinking block never reaches the document.
|
||||
- [ ] Regression suites green in isolation; full suite + coverage >90% + ruff + pyright clean.
|
||||
- [ ] One atomic `--no-gpg-sign` commit; phase dir moved to `.agents/phases/complete/`.
|
||||
Reference in New Issue
Block a user