From ecc921098a8c00dceb5403c0e0b67374f4b5bf32 Mon Sep 17 00:00:00 2001
From: ducoterra
Date: Sat, 12 Sep 2026 19:45:00 -0400
Subject: [PATCH] phase: 104_chip_sizing_question_cap
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
All completion criteria verified. Final report:
**Phase 104 — final verification pass: all green**
- Verified (no code changes needed): single-line ellipsized `.suggestion-chip` CSS + deleted `.maybe-try` override, `renderChips` full-text `title` + `aria-label`-when-clipped, `maxlength="4000"` + `#char-count` counter + `handleSend` over-cap guard, unit pins file, schemas boundary pins, dedicated E2E suite.
- E2E (isolation): `uv run pytest tests/e2e/test_chip_sizing_question_cap.py -v --no-cov` → **6 passed**; regressions: `test_suggestion_chips.py` 8 passed, `test_pinned_composer.py` 4 passed, `test_responsive_polish.py` 7 passed, `test_chat_history.py` 5 passed.
- `uv run pytest` → **2102 passed**; `--cov=app` → **99%** (>90%); `uv run ruff check . && uv run pyright` → clean, 0 errors.
- Criteria: chip E2E (single-line, clipped, title+aria-label full text) ✅; paste caps at exactly 4,000, send streams, counter hides ✅; programmatic 5,000-char fill → banner, no turn, text kept ✅; 4,000/4,001 boundary pinned + HTML maxlength == JS constant cross-file pin ✅.
- Diff scope: `frontend/`, new unit file, `tests/unit/test_schemas.py`, new E2E file, phase files — **no `app/` diff, no migration, no `shared.js` diff**.
- Deviations: 4 regression test files touched — 2 genuine DOM-pin conflicts from the new `#char-count` child (explicitly anticipated by the overview) + 3 documented **pre-existing E2E flake fixes** (smooth-scroll race, tab-walk heuristic, 10 ms timeout), each verified pre-existing on the pre-phase-104 tree.
- No commit made (harness commits per the execution protocol override).
- Next pending phase: `98_sync_summary_visibility`.
---
.../104_chip_sizing_question_cap/00_phase.md | 0
.../01_chip_ellipsis_css.md | 0
.../02_chip_tooltip_aria.md | 0
.../03_composer_question_cap.md | 0
.../04_e2e_suite_commit.md | 0
..._chip_sizing_question_cap__00_phase.a1.err | 0
...4_chip_sizing_question_cap__00_phase.a1.md | 11 +
..._sizing_question_cap__00_phase.a1.validate | 97 +++
..._question_cap__01_chip_ellipsis_css.a1.err | 0
...g_question_cap__01_chip_ellipsis_css.a1.md | 18 +
...tion_cap__01_chip_ellipsis_css.a1.validate | 97 +++
..._question_cap__02_chip_tooltip_aria.a1.err | 0
...g_question_cap__02_chip_tooltip_aria.a1.md | 15 +
...tion_cap__02_chip_tooltip_aria.a1.validate | 97 +++
...stion_cap__03_composer_question_cap.a1.err | 0
...estion_cap__03_composer_question_cap.a1.md | 14 +
..._cap__03_composer_question_cap.a1.validate | 97 +++
...g_question_cap__04_e2e_suite_commit.a1.err | 0
...ng_question_cap__04_e2e_suite_commit.a1.md | 11 +
...stion_cap__04_e2e_suite_commit.a1.validate | 97 +++
frontend/assets/app.js | 55 +-
frontend/assets/styles.css | 39 +-
frontend/index.html | 13 +
tests/e2e/test_chip_sizing_question_cap.py | 521 +++++++++++++++++
tests/e2e/test_pinned_composer.py | 51 +-
tests/e2e/test_responsive_polish.py | 34 +-
tests/e2e/test_suggestion_chips.py | 7 +-
tests/unit/test_chip_sizing_question_cap.py | 550 ++++++++++++++++++
tests/unit/test_pinned_composer.py | 33 +-
tests/unit/test_save_chat_ui.py | 27 +-
tests/unit/test_schemas.py | 26 +
31 files changed, 1874 insertions(+), 36 deletions(-)
rename .agents/phases/{todo => complete}/104_chip_sizing_question_cap/00_phase.md (100%)
rename .agents/phases/{todo => complete}/104_chip_sizing_question_cap/01_chip_ellipsis_css.md (100%)
rename .agents/phases/{todo => complete}/104_chip_sizing_question_cap/02_chip_tooltip_aria.md (100%)
rename .agents/phases/{todo => complete}/104_chip_sizing_question_cap/03_composer_question_cap.md (100%)
rename .agents/phases/{todo => complete}/104_chip_sizing_question_cap/04_e2e_suite_commit.md (100%)
create mode 100644 .agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__00_phase.a1.err
create mode 100644 .agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__00_phase.a1.md
create mode 100644 .agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__00_phase.a1.validate
create mode 100644 .agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__01_chip_ellipsis_css.a1.err
create mode 100644 .agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__01_chip_ellipsis_css.a1.md
create mode 100644 .agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__01_chip_ellipsis_css.a1.validate
create mode 100644 .agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__02_chip_tooltip_aria.a1.err
create mode 100644 .agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__02_chip_tooltip_aria.a1.md
create mode 100644 .agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__02_chip_tooltip_aria.a1.validate
create mode 100644 .agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__03_composer_question_cap.a1.err
create mode 100644 .agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__03_composer_question_cap.a1.md
create mode 100644 .agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__03_composer_question_cap.a1.validate
create mode 100644 .agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__04_e2e_suite_commit.a1.err
create mode 100644 .agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__04_e2e_suite_commit.a1.md
create mode 100644 .agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__04_e2e_suite_commit.a1.validate
create mode 100644 tests/e2e/test_chip_sizing_question_cap.py
create mode 100644 tests/unit/test_chip_sizing_question_cap.py
diff --git a/.agents/phases/todo/104_chip_sizing_question_cap/00_phase.md b/.agents/phases/complete/104_chip_sizing_question_cap/00_phase.md
similarity index 100%
rename from .agents/phases/todo/104_chip_sizing_question_cap/00_phase.md
rename to .agents/phases/complete/104_chip_sizing_question_cap/00_phase.md
diff --git a/.agents/phases/todo/104_chip_sizing_question_cap/01_chip_ellipsis_css.md b/.agents/phases/complete/104_chip_sizing_question_cap/01_chip_ellipsis_css.md
similarity index 100%
rename from .agents/phases/todo/104_chip_sizing_question_cap/01_chip_ellipsis_css.md
rename to .agents/phases/complete/104_chip_sizing_question_cap/01_chip_ellipsis_css.md
diff --git a/.agents/phases/todo/104_chip_sizing_question_cap/02_chip_tooltip_aria.md b/.agents/phases/complete/104_chip_sizing_question_cap/02_chip_tooltip_aria.md
similarity index 100%
rename from .agents/phases/todo/104_chip_sizing_question_cap/02_chip_tooltip_aria.md
rename to .agents/phases/complete/104_chip_sizing_question_cap/02_chip_tooltip_aria.md
diff --git a/.agents/phases/todo/104_chip_sizing_question_cap/03_composer_question_cap.md b/.agents/phases/complete/104_chip_sizing_question_cap/03_composer_question_cap.md
similarity index 100%
rename from .agents/phases/todo/104_chip_sizing_question_cap/03_composer_question_cap.md
rename to .agents/phases/complete/104_chip_sizing_question_cap/03_composer_question_cap.md
diff --git a/.agents/phases/todo/104_chip_sizing_question_cap/04_e2e_suite_commit.md b/.agents/phases/complete/104_chip_sizing_question_cap/04_e2e_suite_commit.md
similarity index 100%
rename from .agents/phases/todo/104_chip_sizing_question_cap/04_e2e_suite_commit.md
rename to .agents/phases/complete/104_chip_sizing_question_cap/04_e2e_suite_commit.md
diff --git a/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__00_phase.a1.err b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__00_phase.a1.err
new file mode 100644
index 0000000..e69de29
diff --git a/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__00_phase.a1.md b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__00_phase.a1.md
new file mode 100644
index 0000000..fd193f6
--- /dev/null
+++ b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__00_phase.a1.md
@@ -0,0 +1,11 @@
+All completion criteria verified. Final report:
+
+**Phase 104 — final verification pass: all green**
+- Verified (no code changes needed): single-line ellipsized `.suggestion-chip` CSS + deleted `.maybe-try` override, `renderChips` full-text `title` + `aria-label`-when-clipped, `maxlength="4000"` + `#char-count` counter + `handleSend` over-cap guard, unit pins file, schemas boundary pins, dedicated E2E suite.
+- E2E (isolation): `uv run pytest tests/e2e/test_chip_sizing_question_cap.py -v --no-cov` → **6 passed**; regressions: `test_suggestion_chips.py` 8 passed, `test_pinned_composer.py` 4 passed, `test_responsive_polish.py` 7 passed, `test_chat_history.py` 5 passed.
+- `uv run pytest` → **2102 passed**; `--cov=app` → **99%** (>90%); `uv run ruff check . && uv run pyright` → clean, 0 errors.
+- Criteria: chip E2E (single-line, clipped, title+aria-label full text) ✅; paste caps at exactly 4,000, send streams, counter hides ✅; programmatic 5,000-char fill → banner, no turn, text kept ✅; 4,000/4,001 boundary pinned + HTML maxlength == JS constant cross-file pin ✅.
+- Diff scope: `frontend/`, new unit file, `tests/unit/test_schemas.py`, new E2E file, phase files — **no `app/` diff, no migration, no `shared.js` diff**.
+- Deviations: 4 regression test files touched — 2 genuine DOM-pin conflicts from the new `#char-count` child (explicitly anticipated by the overview) + 3 documented **pre-existing E2E flake fixes** (smooth-scroll race, tab-walk heuristic, 10 ms timeout), each verified pre-existing on the pre-phase-104 tree.
+- No commit made (harness commits per the execution protocol override).
+- Next pending phase: `98_sync_summary_visibility`.
diff --git a/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__00_phase.a1.validate b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__00_phase.a1.validate
new file mode 100644
index 0000000..15c88de
--- /dev/null
+++ b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__00_phase.a1.validate
@@ -0,0 +1,97 @@
+........................................................................ [ 3%]
+........................................................................ [ 6%]
+........................................................................ [ 10%]
+........................................................................ [ 13%]
+........................................................................ [ 17%]
+........................................................................ [ 20%]
+........................................................................ [ 23%]
+........................................................................ [ 27%]
+........................................................................ [ 30%]
+........................................................................ [ 34%]
+........................................................................ [ 37%]
+........................................................................ [ 41%]
+........................................................................ [ 44%]
+........................................................................ [ 47%]
+........................................................................ [ 51%]
+........................................................................ [ 54%]
+........................................................................ [ 58%]
+........................................................................ [ 61%]
+........................................................................ [ 65%]
+........................................................................ [ 68%]
+........................................................................ [ 71%]
+........................................................................ [ 75%]
+........................................................................ [ 78%]
+........................................................................ [ 82%]
+........................................................................ [ 85%]
+........................................................................ [ 89%]
+........................................................................ [ 92%]
+........................................................................ [ 95%]
+........................................................................ [ 99%]
+.............. [100%]
+=============================== warnings summary ===============================
+.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
+ /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
+ from starlette.testclient import TestClient as TestClient # noqa
+
+-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
+================================ tests coverage ================================
+_______________ coverage: platform linux, python 3.13.13-final-0 _______________
+
+Name Stmts Miss Cover
+--------------------------------------------------
+app/__init__.py 1 0 100%
+app/api/__init__.py 0 0 100%
+app/api/auth.py 52 0 100%
+app/api/chat.py 181 0 100%
+app/api/chats.py 110 0 100%
+app/api/config.py 13 0 100%
+app/api/doc_drafts.py 94 0 100%
+app/api/docs.py 134 1 99%
+app/api/git_sources.py 229 0 100%
+app/api/health.py 10 0 100%
+app/api/steering.py 42 0 100%
+app/api/suggestions.py 33 0 100%
+app/api/sync.py 115 0 100%
+app/api/tokens.py 40 0 100%
+app/api/ui_settings.py 55 0 100%
+app/config.py 140 0 100%
+app/core/__init__.py 0 0 100%
+app/core/auth.py 45 0 100%
+app/core/caching.py 124 0 100%
+app/core/debugging.py 29 2 93%
+app/core/docs_push.py 39 0 100%
+app/core/errors.py 5 0 100%
+app/core/logging.py 13 0 100%
+app/core/rate_limit.py 44 0 100%
+app/core/security_headers.py 20 0 100%
+app/core/theming.py 38 0 100%
+app/core/tokens.py 44 0 100%
+app/db.py 21 0 100%
+app/main.py 66 0 100%
+app/models.py 125 0 100%
+app/rag/__init__.py 0 0 100%
+app/rag/agent.py 316 1 99%
+app/rag/archive_upload.py 128 0 100%
+app/rag/chunker.py 206 4 98%
+app/rag/folder_summaries.py 121 0 100%
+app/rag/git_sources.py 14 0 100%
+app/rag/importer.py 197 3 98%
+app/rag/llm.py 243 1 99%
+app/rag/overview.py 71 0 100%
+app/rag/prompts.py 88 0 100%
+app/rag/retriever.py 150 3 98%
+app/rag/scaffolding.py 55 0 100%
+app/rag/source_removal.py 41 0 100%
+app/rag/sources_meta.py 16 0 100%
+app/rag/suggestions.py 27 0 100%
+app/rag/summarizer.py 24 0 100%
+app/schemas.py 307 0 100%
+--------------------------------------------------
+TOTAL 3866 15 99%
+coverage gate: app/ 99% (>90%) OK
+All checks passed!
+0 errors, 0 warnings, 0 informations
+WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
+Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
+
+validation OK
diff --git a/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__01_chip_ellipsis_css.a1.err b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__01_chip_ellipsis_css.a1.err
new file mode 100644
index 0000000..e69de29
diff --git a/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__01_chip_ellipsis_css.a1.md b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__01_chip_ellipsis_css.a1.md
new file mode 100644
index 0000000..bdba8c7
--- /dev/null
+++ b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__01_chip_ellipsis_css.a1.md
@@ -0,0 +1,18 @@
+Task 01 complete — all gates green.
+
+**Implemented**
+- `frontend/assets/styles.css`: base `.suggestion-chip` rule gains the single-line contract (`white-space: nowrap`, `overflow: hidden`, `text-overflow: ellipsis`, `max-width: 100%`, `min-width: 0`) with the phase-104 provenance comment (incl. the folded-in phase-07 origin).
+- Deleted the fully subsumed `.maybe-try .suggestion-chip` override (its phase-07 provenance lives in the base rule's comment, phrased without the literal selector so the `count == 0` pin holds).
+- Created `tests/unit/test_chip_sizing_question_cap.py` — the phase's unit-pins file with the three task-01 CSS pins (ellipsis quartet + pill contract kept, override gone / `.maybe-try` group stays, ≤640px `flex: 0 0 auto` track untouched).
+- Mobile rule and `.shared-shell` static-chip rule untouched (A6); no JS/HTML/E2E changes (later tasks).
+
+**Test / lint / coverage**
+- `uv run pytest tests/unit/ -q` → 1564 passed
+- `uv run pytest` (unit+integration, DB up) → 2089 passed
+- `uv run pytest --cov=app --cov-report=term-missing` → TOTAL 99% (>90% gate)
+- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings
+- `git diff --stat` → `frontend/assets/styles.css` only (+ new unit file; no `app/` diff)
+
+**Decisions:** none deviating — followed the phase design verbatim.
+
+**Next pending task:** `.agents/phases/todo/104_chip_sizing_question_cap/02_chip_tooltip_aria.md`
diff --git a/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__01_chip_ellipsis_css.a1.validate b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__01_chip_ellipsis_css.a1.validate
new file mode 100644
index 0000000..9ca2a44
--- /dev/null
+++ b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__01_chip_ellipsis_css.a1.validate
@@ -0,0 +1,97 @@
+........................................................................ [ 3%]
+........................................................................ [ 6%]
+........................................................................ [ 10%]
+........................................................................ [ 13%]
+........................................................................ [ 17%]
+........................................................................ [ 20%]
+........................................................................ [ 24%]
+........................................................................ [ 27%]
+........................................................................ [ 31%]
+........................................................................ [ 34%]
+........................................................................ [ 37%]
+........................................................................ [ 41%]
+........................................................................ [ 44%]
+........................................................................ [ 48%]
+........................................................................ [ 51%]
+........................................................................ [ 55%]
+........................................................................ [ 58%]
+........................................................................ [ 62%]
+........................................................................ [ 65%]
+........................................................................ [ 68%]
+........................................................................ [ 72%]
+........................................................................ [ 75%]
+........................................................................ [ 79%]
+........................................................................ [ 82%]
+........................................................................ [ 86%]
+........................................................................ [ 89%]
+........................................................................ [ 93%]
+........................................................................ [ 96%]
+........................................................................ [ 99%]
+. [100%]
+=============================== warnings summary ===============================
+.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
+ /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
+ from starlette.testclient import TestClient as TestClient # noqa
+
+-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
+================================ tests coverage ================================
+_______________ coverage: platform linux, python 3.13.13-final-0 _______________
+
+Name Stmts Miss Cover
+--------------------------------------------------
+app/__init__.py 1 0 100%
+app/api/__init__.py 0 0 100%
+app/api/auth.py 52 0 100%
+app/api/chat.py 181 0 100%
+app/api/chats.py 110 0 100%
+app/api/config.py 13 0 100%
+app/api/doc_drafts.py 94 0 100%
+app/api/docs.py 134 1 99%
+app/api/git_sources.py 229 0 100%
+app/api/health.py 10 0 100%
+app/api/steering.py 42 0 100%
+app/api/suggestions.py 33 0 100%
+app/api/sync.py 115 0 100%
+app/api/tokens.py 40 0 100%
+app/api/ui_settings.py 55 0 100%
+app/config.py 140 0 100%
+app/core/__init__.py 0 0 100%
+app/core/auth.py 45 0 100%
+app/core/caching.py 124 0 100%
+app/core/debugging.py 29 2 93%
+app/core/docs_push.py 39 0 100%
+app/core/errors.py 5 0 100%
+app/core/logging.py 13 0 100%
+app/core/rate_limit.py 44 0 100%
+app/core/security_headers.py 20 0 100%
+app/core/theming.py 38 0 100%
+app/core/tokens.py 44 0 100%
+app/db.py 21 0 100%
+app/main.py 66 0 100%
+app/models.py 125 0 100%
+app/rag/__init__.py 0 0 100%
+app/rag/agent.py 316 1 99%
+app/rag/archive_upload.py 128 0 100%
+app/rag/chunker.py 206 4 98%
+app/rag/folder_summaries.py 121 0 100%
+app/rag/git_sources.py 14 0 100%
+app/rag/importer.py 197 3 98%
+app/rag/llm.py 243 1 99%
+app/rag/overview.py 71 0 100%
+app/rag/prompts.py 88 0 100%
+app/rag/retriever.py 150 3 98%
+app/rag/scaffolding.py 55 0 100%
+app/rag/source_removal.py 41 0 100%
+app/rag/sources_meta.py 16 0 100%
+app/rag/suggestions.py 27 0 100%
+app/rag/summarizer.py 24 0 100%
+app/schemas.py 307 0 100%
+--------------------------------------------------
+TOTAL 3866 15 99%
+coverage gate: app/ 99% (>90%) OK
+All checks passed!
+0 errors, 0 warnings, 0 informations
+WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
+Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
+
+validation OK
diff --git a/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__02_chip_tooltip_aria.a1.err b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__02_chip_tooltip_aria.a1.err
new file mode 100644
index 0000000..e69de29
diff --git a/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__02_chip_tooltip_aria.a1.md b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__02_chip_tooltip_aria.a1.md
new file mode 100644
index 0000000..a538074
--- /dev/null
+++ b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__02_chip_tooltip_aria.a1.md
@@ -0,0 +1,15 @@
+Task 02 complete. All checks green.
+
+**Implemented**
+- `frontend/assets/app.js` `renderChips`: `btn.title = text` on every chip (full-text hover tooltip, owner A2, source-chip precedent) + `aria-label` = full text only when `btn.scrollWidth > btn.clientWidth` (clipped case), with the phase-104 provenance comment; nothing else in `renderChips` touched
+- `tests/unit/test_chip_sizing_question_cap.py`: added `APP_JS`/`_app_js()`/`_render_chips_body()` (brace-matched extraction, handles the `{ onSelect }` destructuring) + two task-02 pins (`btn.title = text`; the guard-anchored `setAttribute("aria-label", text)`) + module docstring updated
+
+**Results**
+- `uv run pytest tests/unit/` → 1566 passed
+- `uv run pytest` (unit + integration) → 2091 passed
+- `uv run pytest --cov=app --cov-report=term` → 2091 passed, `app/` coverage 99% (>90% gate)
+- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors
+
+**Decisions:** `shared.js` untouched (its "no renderChips" pin stays green — it guards a different file).
+
+**Next pending task:** `.agents/phases/todo/104_chip_sizing_question_cap/03_composer_question_cap.md`
diff --git a/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__02_chip_tooltip_aria.a1.validate b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__02_chip_tooltip_aria.a1.validate
new file mode 100644
index 0000000..98f85ea
--- /dev/null
+++ b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__02_chip_tooltip_aria.a1.validate
@@ -0,0 +1,97 @@
+........................................................................ [ 3%]
+........................................................................ [ 6%]
+........................................................................ [ 10%]
+........................................................................ [ 13%]
+........................................................................ [ 17%]
+........................................................................ [ 20%]
+........................................................................ [ 24%]
+........................................................................ [ 27%]
+........................................................................ [ 30%]
+........................................................................ [ 34%]
+........................................................................ [ 37%]
+........................................................................ [ 41%]
+........................................................................ [ 44%]
+........................................................................ [ 48%]
+........................................................................ [ 51%]
+........................................................................ [ 55%]
+........................................................................ [ 58%]
+........................................................................ [ 61%]
+........................................................................ [ 65%]
+........................................................................ [ 68%]
+........................................................................ [ 72%]
+........................................................................ [ 75%]
+........................................................................ [ 79%]
+........................................................................ [ 82%]
+........................................................................ [ 86%]
+........................................................................ [ 89%]
+........................................................................ [ 92%]
+........................................................................ [ 96%]
+........................................................................ [ 99%]
+... [100%]
+=============================== warnings summary ===============================
+.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
+ /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
+ from starlette.testclient import TestClient as TestClient # noqa
+
+-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
+================================ tests coverage ================================
+_______________ coverage: platform linux, python 3.13.13-final-0 _______________
+
+Name Stmts Miss Cover
+--------------------------------------------------
+app/__init__.py 1 0 100%
+app/api/__init__.py 0 0 100%
+app/api/auth.py 52 0 100%
+app/api/chat.py 181 0 100%
+app/api/chats.py 110 0 100%
+app/api/config.py 13 0 100%
+app/api/doc_drafts.py 94 0 100%
+app/api/docs.py 134 1 99%
+app/api/git_sources.py 229 0 100%
+app/api/health.py 10 0 100%
+app/api/steering.py 42 0 100%
+app/api/suggestions.py 33 0 100%
+app/api/sync.py 115 0 100%
+app/api/tokens.py 40 0 100%
+app/api/ui_settings.py 55 0 100%
+app/config.py 140 0 100%
+app/core/__init__.py 0 0 100%
+app/core/auth.py 45 0 100%
+app/core/caching.py 124 0 100%
+app/core/debugging.py 29 2 93%
+app/core/docs_push.py 39 0 100%
+app/core/errors.py 5 0 100%
+app/core/logging.py 13 0 100%
+app/core/rate_limit.py 44 0 100%
+app/core/security_headers.py 20 0 100%
+app/core/theming.py 38 0 100%
+app/core/tokens.py 44 0 100%
+app/db.py 21 0 100%
+app/main.py 66 0 100%
+app/models.py 125 0 100%
+app/rag/__init__.py 0 0 100%
+app/rag/agent.py 316 1 99%
+app/rag/archive_upload.py 128 0 100%
+app/rag/chunker.py 206 4 98%
+app/rag/folder_summaries.py 121 0 100%
+app/rag/git_sources.py 14 0 100%
+app/rag/importer.py 197 3 98%
+app/rag/llm.py 243 1 99%
+app/rag/overview.py 71 0 100%
+app/rag/prompts.py 88 0 100%
+app/rag/retriever.py 150 3 98%
+app/rag/scaffolding.py 55 0 100%
+app/rag/source_removal.py 41 0 100%
+app/rag/sources_meta.py 16 0 100%
+app/rag/suggestions.py 27 0 100%
+app/rag/summarizer.py 24 0 100%
+app/schemas.py 307 0 100%
+--------------------------------------------------
+TOTAL 3866 15 99%
+coverage gate: app/ 99% (>90%) OK
+All checks passed!
+0 errors, 0 warnings, 0 informations
+WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
+Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
+
+validation OK
diff --git a/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__03_composer_question_cap.a1.err b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__03_composer_question_cap.a1.err
new file mode 100644
index 0000000..e69de29
diff --git a/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__03_composer_question_cap.a1.md b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__03_composer_question_cap.a1.md
new file mode 100644
index 0000000..a2a2ccd
--- /dev/null
+++ b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__03_composer_question_cap.a1.md
@@ -0,0 +1,14 @@
+All gates green. Final report:
+
+**Task 03 complete — visible 4,000-char question cap (maxlength + counter + guard + server-boundary pin)**
+
+- `#message-input` gains `maxlength="4000"` with provenance comment; `#char-count` counter added inside `.chat-bottom` between the actions row and composer, hidden by default, with the phase-104 decision comment (not a live region; B3 text+color)
+- `app.js`: `MAX_QUESTION_CHARS=4000` / `CHAR_COUNT_SHOW_AT=3200` (80%), `updateCharCount()` (RAW length, `.is-max` + "— character limit" copy at/over cap, honest over-cap reading), called at all four `input.value` mutation sites; `handleSend` over-cap guard (banner, no turn, text kept) before the clear
+- `styles.css`: `.char-count` / `.char-count.is-max` rules — AA-verified on `--bg` (8.6:1, 10.4:1) with ratios recorded in the comment
+- Unit pins: `test_chip_sizing_question_cap.py` extended (HTML/JS/CSS pins + single-source cross-file pin HTML==JS==schema); `test_schemas.py` 4,000/4,001 `ChatRequest.message` boundary; two conflicting unit pins (`test_pinned_composer.py`, `test_save_chat_ui.py`) updated to carry the new counter child (genuine conflicts anticipated by the phase)
+- **Defect fixes in prior work:** `e2e/test_pinned_composer.py::test_stop_is_reachable_from_scrolled_up` was racy (proven on the pristine tree — the test scrolled up mid smooth-`scrollReveal`; reads caught y=23–119) → deterministic arrival-wait on the captured scroll target; `e2e/test_suggestion_chips.py` `_api_suggestions` `timeout=10` (10 ms) sat below the ~12 ms steady-state localhost round trip → 10 s house pattern
+
+- Results: `uv run pytest tests/unit/ -q` → 1577 passed; `uv run pytest --cov=app` → 2102 passed, **99%** coverage (>90%); `uv run ruff check .` → clean; `uv run pyright` → 0 errors
+- Regression E2Es (isolated): `test_pinned_composer.py` 4 passed (×6 consecutive), `test_suggestion_chips.py` 8 passed (×2), `test_responsive_polish.py` 7 passed, `test_chat_history.py` 5 passed; no `app/` diff
+
+- Next pending task: `04_e2e_suite_commit.md` (dedicated E2E suite + full gate + atomic commit)
diff --git a/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__03_composer_question_cap.a1.validate b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__03_composer_question_cap.a1.validate
new file mode 100644
index 0000000..15c88de
--- /dev/null
+++ b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__03_composer_question_cap.a1.validate
@@ -0,0 +1,97 @@
+........................................................................ [ 3%]
+........................................................................ [ 6%]
+........................................................................ [ 10%]
+........................................................................ [ 13%]
+........................................................................ [ 17%]
+........................................................................ [ 20%]
+........................................................................ [ 23%]
+........................................................................ [ 27%]
+........................................................................ [ 30%]
+........................................................................ [ 34%]
+........................................................................ [ 37%]
+........................................................................ [ 41%]
+........................................................................ [ 44%]
+........................................................................ [ 47%]
+........................................................................ [ 51%]
+........................................................................ [ 54%]
+........................................................................ [ 58%]
+........................................................................ [ 61%]
+........................................................................ [ 65%]
+........................................................................ [ 68%]
+........................................................................ [ 71%]
+........................................................................ [ 75%]
+........................................................................ [ 78%]
+........................................................................ [ 82%]
+........................................................................ [ 85%]
+........................................................................ [ 89%]
+........................................................................ [ 92%]
+........................................................................ [ 95%]
+........................................................................ [ 99%]
+.............. [100%]
+=============================== warnings summary ===============================
+.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
+ /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
+ from starlette.testclient import TestClient as TestClient # noqa
+
+-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
+================================ tests coverage ================================
+_______________ coverage: platform linux, python 3.13.13-final-0 _______________
+
+Name Stmts Miss Cover
+--------------------------------------------------
+app/__init__.py 1 0 100%
+app/api/__init__.py 0 0 100%
+app/api/auth.py 52 0 100%
+app/api/chat.py 181 0 100%
+app/api/chats.py 110 0 100%
+app/api/config.py 13 0 100%
+app/api/doc_drafts.py 94 0 100%
+app/api/docs.py 134 1 99%
+app/api/git_sources.py 229 0 100%
+app/api/health.py 10 0 100%
+app/api/steering.py 42 0 100%
+app/api/suggestions.py 33 0 100%
+app/api/sync.py 115 0 100%
+app/api/tokens.py 40 0 100%
+app/api/ui_settings.py 55 0 100%
+app/config.py 140 0 100%
+app/core/__init__.py 0 0 100%
+app/core/auth.py 45 0 100%
+app/core/caching.py 124 0 100%
+app/core/debugging.py 29 2 93%
+app/core/docs_push.py 39 0 100%
+app/core/errors.py 5 0 100%
+app/core/logging.py 13 0 100%
+app/core/rate_limit.py 44 0 100%
+app/core/security_headers.py 20 0 100%
+app/core/theming.py 38 0 100%
+app/core/tokens.py 44 0 100%
+app/db.py 21 0 100%
+app/main.py 66 0 100%
+app/models.py 125 0 100%
+app/rag/__init__.py 0 0 100%
+app/rag/agent.py 316 1 99%
+app/rag/archive_upload.py 128 0 100%
+app/rag/chunker.py 206 4 98%
+app/rag/folder_summaries.py 121 0 100%
+app/rag/git_sources.py 14 0 100%
+app/rag/importer.py 197 3 98%
+app/rag/llm.py 243 1 99%
+app/rag/overview.py 71 0 100%
+app/rag/prompts.py 88 0 100%
+app/rag/retriever.py 150 3 98%
+app/rag/scaffolding.py 55 0 100%
+app/rag/source_removal.py 41 0 100%
+app/rag/sources_meta.py 16 0 100%
+app/rag/suggestions.py 27 0 100%
+app/rag/summarizer.py 24 0 100%
+app/schemas.py 307 0 100%
+--------------------------------------------------
+TOTAL 3866 15 99%
+coverage gate: app/ 99% (>90%) OK
+All checks passed!
+0 errors, 0 warnings, 0 informations
+WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
+Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
+
+validation OK
diff --git a/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__04_e2e_suite_commit.a1.err b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__04_e2e_suite_commit.a1.err
new file mode 100644
index 0000000..e69de29
diff --git a/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__04_e2e_suite_commit.a1.md b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__04_e2e_suite_commit.a1.md
new file mode 100644
index 0000000..c54e8d2
--- /dev/null
+++ b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__04_e2e_suite_commit.a1.md
@@ -0,0 +1,11 @@
+All gates green. Final report:
+
+**Phase 104 · Task 04 — E2E suite + regression gates**
+
+- New dedicated suite `tests/e2e/test_chip_sizing_question_cap.py` (6 tests: single-line ellipsized long chip with `title`/`aria-label` full text, seed-chip tooltip-without-aria contrast, counter threshold 100/3500, paste-path hard cap at 4,000 + clean submit at cap, over-cap programmatic-fill guard (banner, no turn, text kept), short-flow never-visible counter with MutationObserver tripwire) — drafted in the interrupted prior attempt, verified green
+- Fixed a **pre-existing flake** in `test_responsive_polish.py::test_a11y_landmarks_and_labels` (verified identical on the pristine pre-phase-104 tree): `_tab_outline_walk` keyed elements by 24-char text prefix, so two identical opener chips left in shared-DB `saved_chats` by `test_pinned_composer.py` cut the walk short (2 < 3); now uses true element identity via a stamped `data-tabwalk-id`. No behavior change to the asserted contract
+- E2E in isolation: `uv run pytest tests/e2e/test_chip_sizing_question_cap.py -v --no-cov` → 6 passed; regressions each green in isolation: `test_suggestion_chips` 8, `test_pinned_composer` 4, `test_responsive_polish` 7 (incl. adversarial post-pinned_composer state, 3 runs), `test_chat_history` 5
+- Full gate: `uv run pytest` → 2102 passed; `uv run pytest --cov=app --cov-report=term-missing` → 2102 passed, `app/` **99%** (>90%); `uv run ruff check . && uv run pyright` → clean, 0 errors
+- `git diff --stat` scoped: `frontend/` (app.js/styles.css/index.html), new unit+E2E files, `test_schemas.py`, pin updates (`test_pinned_composer` unit, `test_save_chat_ui`), prior-task e2e flake fixes (`test_pinned_composer`, `test_suggestion_chips`) + this fix; NO `app/` code diff, no migration, no `shared.js`, no `pyproject.toml`/`uv.lock`
+- Phase dir move + atomic `--no-gpg-sign` commit: left to the harness per executor rules (overrides task step 5)
+- Next pending task: none in this phase (04 is the last); next phase = whatever follows `104` in `.agents/phases/todo/`
diff --git a/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__04_e2e_suite_commit.a1.validate b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__04_e2e_suite_commit.a1.validate
new file mode 100644
index 0000000..15c88de
--- /dev/null
+++ b/.agents/reports/104_chip_sizing_question_cap/104_chip_sizing_question_cap__04_e2e_suite_commit.a1.validate
@@ -0,0 +1,97 @@
+........................................................................ [ 3%]
+........................................................................ [ 6%]
+........................................................................ [ 10%]
+........................................................................ [ 13%]
+........................................................................ [ 17%]
+........................................................................ [ 20%]
+........................................................................ [ 23%]
+........................................................................ [ 27%]
+........................................................................ [ 30%]
+........................................................................ [ 34%]
+........................................................................ [ 37%]
+........................................................................ [ 41%]
+........................................................................ [ 44%]
+........................................................................ [ 47%]
+........................................................................ [ 51%]
+........................................................................ [ 54%]
+........................................................................ [ 58%]
+........................................................................ [ 61%]
+........................................................................ [ 65%]
+........................................................................ [ 68%]
+........................................................................ [ 71%]
+........................................................................ [ 75%]
+........................................................................ [ 78%]
+........................................................................ [ 82%]
+........................................................................ [ 85%]
+........................................................................ [ 89%]
+........................................................................ [ 92%]
+........................................................................ [ 95%]
+........................................................................ [ 99%]
+.............. [100%]
+=============================== warnings summary ===============================
+.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
+ /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
+ from starlette.testclient import TestClient as TestClient # noqa
+
+-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
+================================ tests coverage ================================
+_______________ coverage: platform linux, python 3.13.13-final-0 _______________
+
+Name Stmts Miss Cover
+--------------------------------------------------
+app/__init__.py 1 0 100%
+app/api/__init__.py 0 0 100%
+app/api/auth.py 52 0 100%
+app/api/chat.py 181 0 100%
+app/api/chats.py 110 0 100%
+app/api/config.py 13 0 100%
+app/api/doc_drafts.py 94 0 100%
+app/api/docs.py 134 1 99%
+app/api/git_sources.py 229 0 100%
+app/api/health.py 10 0 100%
+app/api/steering.py 42 0 100%
+app/api/suggestions.py 33 0 100%
+app/api/sync.py 115 0 100%
+app/api/tokens.py 40 0 100%
+app/api/ui_settings.py 55 0 100%
+app/config.py 140 0 100%
+app/core/__init__.py 0 0 100%
+app/core/auth.py 45 0 100%
+app/core/caching.py 124 0 100%
+app/core/debugging.py 29 2 93%
+app/core/docs_push.py 39 0 100%
+app/core/errors.py 5 0 100%
+app/core/logging.py 13 0 100%
+app/core/rate_limit.py 44 0 100%
+app/core/security_headers.py 20 0 100%
+app/core/theming.py 38 0 100%
+app/core/tokens.py 44 0 100%
+app/db.py 21 0 100%
+app/main.py 66 0 100%
+app/models.py 125 0 100%
+app/rag/__init__.py 0 0 100%
+app/rag/agent.py 316 1 99%
+app/rag/archive_upload.py 128 0 100%
+app/rag/chunker.py 206 4 98%
+app/rag/folder_summaries.py 121 0 100%
+app/rag/git_sources.py 14 0 100%
+app/rag/importer.py 197 3 98%
+app/rag/llm.py 243 1 99%
+app/rag/overview.py 71 0 100%
+app/rag/prompts.py 88 0 100%
+app/rag/retriever.py 150 3 98%
+app/rag/scaffolding.py 55 0 100%
+app/rag/source_removal.py 41 0 100%
+app/rag/sources_meta.py 16 0 100%
+app/rag/suggestions.py 27 0 100%
+app/rag/summarizer.py 24 0 100%
+app/schemas.py 307 0 100%
+--------------------------------------------------
+TOTAL 3866 15 99%
+coverage gate: app/ 99% (>90%) OK
+All checks passed!
+0 errors, 0 warnings, 0 informations
+WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
+Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
+
+validation OK
diff --git a/frontend/assets/app.js b/frontend/assets/app.js
index 89c1636..cbb8dd4 100644
--- a/frontend/assets/app.js
+++ b/frontend/assets/app.js
@@ -299,6 +299,7 @@ bindSharedHeaderControls();
const messagesEl = document.querySelector("#messages");
const emptyState = document.querySelector("#empty-state");
const suggestionsEl = document.querySelector("#suggestions");
+const charCountEl = document.querySelector("#char-count"); // phase 104: the question-length counter (ships hidden)
const composer = document.querySelector("#composer");
const input = document.querySelector("#message-input");
const sendBtn = document.querySelector("#send-btn");
@@ -1012,6 +1013,7 @@ function appendTruncatedNote(wrap, argument, charsShown, charsTotal) {
function submitSuggestion(text) {
input.value = text;
autoGrow();
+ updateCharCount(); // phase 104: the chip fill bypasses maxlength — count what landed
input.focus();
composer.requestSubmit();
}
@@ -1028,11 +1030,16 @@ function renderChips(container, items, { onSelect } = {}) {
btn.className = "suggestion-chip";
btn.setAttribute("role", "listitem");
btn.textContent = text;
+ // phase 104 (owner 2026-09-12): the single-line chip clips long
+ // questions — `title` is the hover reveal, `aria-label` the
+ // clipped-case accessible name (the source-chip pattern).
+ btn.title = text;
btn.addEventListener("click", () => {
submitSuggestion(text);
if (onSelect) onSelect(text, btn);
});
container.appendChild(btn);
+ if (btn.scrollWidth > btn.clientWidth) btn.setAttribute("aria-label", text);
}
return container;
}
@@ -1255,11 +1262,42 @@ export function setUiState(state, errorDetail = "") {
if (state === UI_STATE.error) showErrorBanner(errorDetail);
}
+/* Phase 104 (owner 2026-09-12, A3/A4): the visible question-length cap.
+ MAX_QUESTION_CHARS mirrors ChatRequest.message max_length=4000
+ (app/schemas.py — the source of truth; the server 422s beyond it) and
+ must stay equal to the #message-input maxlength (cross-file pin in
+ tests/unit/test_chip_sizing_question_cap.py). The counter shows only
+ from 80% of the cap — no noise on normal use (owner A4). */
+const MAX_QUESTION_CHARS = 4000;
+const CHAR_COUNT_SHOW_AT = 3200; // 80% of the cap — visible only when it matters
+
function autoGrow() {
input.style.height = "auto";
input.style.height = `${Math.min(input.scrollHeight, 192)}px`;
}
+/* Phase 104 (owner 2026-09-12, A4): the counter state — hidden below the
+ 80% threshold, plain `len/4000` above it, and `len/4000 — character
+ limit` + the .is-max (–err-*) treatment at/over the cap. The over-cap
+ reading is the HONEST length (the programmatic chip-fill path can
+ exceed maxlength — e.g. `5123/4000 — character limit`). */
+function updateCharCount() {
+ // RAW length (no trim): raw ≤ cap ⟹ trimmed ≤ cap, so the raw
+ // count is a safe superset of what the server validates.
+ const len = input.value.length;
+ if (len < CHAR_COUNT_SHOW_AT) {
+ charCountEl.hidden = true;
+ charCountEl.classList.remove("is-max");
+ return;
+ }
+ charCountEl.hidden = false;
+ const atMax = len >= MAX_QUESTION_CHARS;
+ charCountEl.classList.toggle("is-max", atMax);
+ charCountEl.textContent = atMax
+ ? `${len}/${MAX_QUESTION_CHARS} — character limit`
+ : `${len}/${MAX_QUESTION_CHARS}`;
+}
+
/* ---------- chat turn (SSE streaming, PLAN §4) ---------- */
/* Cancel a response body without leaking an unhandled rejection:
@@ -2014,6 +2052,7 @@ function startNewChat() {
setUiState(UI_STATE.idle);
input.value = "";
autoGrow();
+ updateCharCount(); // phase 104: the cleared composer hides the counter again
input.focus();
sendStatus.textContent = "New chat started — previous conversation cleared.";
}
@@ -2111,12 +2150,21 @@ async function handleSend(e) {
}
const text = input.value.trim();
if (!text || sendBtn.disabled) return;
+ // Phase 104 (owner 2026-09-12, A5): maxlength caps typing + pastes, but
+ // a programmatic fill (the chip one-tap path) bypasses it — this guard
+ // is the never-stale backstop (PLAN §7.4): no turn, no clear, the user
+ // trims the kept text (out-of-turn banner, the saveAsDoc precedent).
+ if (text.length > MAX_QUESTION_CHARS) {
+ showErrorBanner("Questions are limited to 4,000 characters — trim the question and try again.");
+ return;
+ }
// Phase 49: the user append + persistence save point 1 moved into
// runTurn with the rest of the turn — the `reask` flag skips them on
// the redo-in-place retry path (the question is already in the DOM +
// conversation); handleSend keeps only the form-level pre-work.
input.value = "";
autoGrow();
+ updateCharCount(); // phase 104: the sent question clears the counter with the input
clearErrorBanner();
await runTurn(text, { reask: false });
}
@@ -2481,7 +2529,12 @@ async function runTurn(text, { reask = false } = {}) {
}
}
-input.addEventListener("input", autoGrow);
+// Phase 104: every input-path change (keystroke, paste — maxlength
+// caps both at 4,000) re-runs the counter alongside the auto-grow.
+input.addEventListener("input", () => {
+ autoGrow();
+ updateCharCount();
+});
input.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
diff --git a/frontend/assets/styles.css b/frontend/assets/styles.css
index 323e694..8cd153e 100644
--- a/frontend/assets/styles.css
+++ b/frontend/assets/styles.css
@@ -703,14 +703,6 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
gap: 0.45rem;
padding-inline: 0.25rem;
}
-/* As flex items these chips must be allowed to shrink (min-width:auto
- would let a long title-derived chip exceed the column on phones —
- phase 07 overflow fix); the label text then wraps inside the pill. */
-.maybe-try .suggestion-chip {
- min-width: 0;
- max-width: 100%;
-}
-
/* ---------- Steering notes (phase 15) ---------- */
/* "Tune" button in the meta row of every completed brain bubble: ghost
pill, ≥44px, right-aligned after the source chips. ink-soft on surface
@@ -1230,6 +1222,21 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
margin: 0;
padding: 0;
}
+/* The shared chip pill — both chip rows (the onboarding row in the
+ empty state and the "Maybe try" row under a deflected bubble) reuse
+ it. Phase 104 (owner 2026-09-12, A1): a chip is ONE line at every
+ viewport width — `white-space: nowrap` stops a 400-char question
+ from wrapping the pill into a multi-line "chonk"; `overflow: hidden`
+ zeroes the flex item's automatic minimum size (min-width:auto would
+ pin the item to the full text width), so `max-width: 100%` binds and
+ `text-overflow: ellipsis` clips the text at the row edge — in the
+ desktop wrap row 100% is the chat column, in the ≤640px row
+ (nowrap + overflow-x: auto) 100% is the VISIBLE width and the row
+ scrolls. That clipping is the phase-07 overflow fix the old
+ "Maybe try" chip override used to carry (min-width:0 + max-width:
+ 100%) — now fully subsumed here and deleted, provenance folded into
+ this comment so the history lives with the contract. The full text
+ stays one hover away (the title tooltip, phase 104 task 02). */
.suggestion-chip {
font: inherit;
font-size: 0.92rem;
@@ -1242,6 +1249,11 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
min-height: 44px;
cursor: pointer;
transition: background 0.15s ease, transform 0.05s ease;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 100%;
+ min-width: 0;
}
.suggestion-chip:hover { background: var(--brand-soft); }
.suggestion-chip:active { transform: scale(0.98); }
@@ -1321,6 +1333,17 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
#view-chat:not(.chat-booted) .composer {
position: static;
}
+/* Phase 104 (owner 2026-09-12, A4): the question-length counter —
+ right-aligned above the composer, a child of the .chat-bottom sticky
+ unit (between the actions row and the form), hidden until 80% of the
+ 4,000-char cap (app.js updateCharCount). It sits on the APP
+ background behind the transparent .chat-bottom unit, so both pairings
+ are verified against --bg: --ink-soft on --bg = 8.6:1 (≥4.5:1, WCAG
+ AA) and the .is-max state's --err-ink on --bg = 10.4:1 (AA). The
+ .is-max state ALSO changes the copy ("— character limit") — text +
+ color, never color alone (B3). */
+.char-count { margin: 0; text-align: right; font-size: 0.75rem; line-height: 1.2; color: var(--ink-soft); }
+.char-count.is-max { color: var(--err-ink); }
.composer {
display: flex;
align-items: flex-end;
diff --git a/frontend/index.html b/frontend/index.html
index 4925500..c8d9f69 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -252,6 +252,15 @@
+
+
+
', ""
+ )
assert (
- "id=" not in after
- and "