feat(chat): stop an in-flight answer — Send becomes Stop, the partial is kept and persisted, the model stream is torn down
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
# Task 01 — `git_sources.kind` + `path` (migration 0007)
|
||||
|
||||
**Phase:** `38_local_directory_sources` · **Source:** `TODO.md:11 — "Also need a way to import from existing directory if it's not a git repo"`
|
||||
**Story:** `.agent/user_stories/local-directory-sources.md`
|
||||
|
||||
## Objective
|
||||
Extend the phase-35 `git_sources` table with a source-kind discriminator (`git` | `local`) and an optional local path, via a reversible migration — the foundation for the API, pipeline, and page tasks.
|
||||
|
||||
## Work
|
||||
1. `alembic/versions/0007_git_sources_kind.py` — read `alembic/versions/0006_git_sources.py` first and chain from its **actual** `revision` id (filenames are not revision ids — the phase-35 task-01 convention):
|
||||
- `upgrade()`: `ALTER TABLE git_sources ADD COLUMN kind TEXT NOT NULL DEFAULT 'git'` + `ADD CONSTRAINT ck_git_sources_kind CHECK (kind IN ('git', 'local'))`; `ALTER TABLE git_sources ADD COLUMN path TEXT`; a unique index on `path` — a partial unique index (`WHERE path IS NOT NULL`) where the 0006 style allows it, otherwise a plain unique index (Postgres treats NULLs as distinct, and the API enforces local-only paths anyway — mirror whatever 0006 chose for `url`).
|
||||
- `downgrade()`: drop the index, constraint, and columns in reverse.
|
||||
2. `app/models.py` — extend the phase-35 `GitSource` model: `kind: Mapped[str]` (default `"git"`) + `path: Mapped[str | None]` (nullable, unique) and update the docstring (phase 38: kind discriminator; local rows carry `path`, git rows keep `url`).
|
||||
3. Apply to the dev database: `podman compose up -d db` (if needed) then `uv run alembic upgrade head`; verify the columns + constraint + that existing rows read as `kind='git'`, `path=NULL`.
|
||||
4. Migration test — follow the 0004/0005/0006 pattern in `tests/integration/`: up adds the columns/constraint/index (existing rows keep `kind='git'`), down drops them (round-trip on the test DB).
|
||||
|
||||
## Testing & Quality
|
||||
- Integration: the up/down test above; full `uv run pytest` green.
|
||||
- Coverage: model + migration only — the `app/` gate stays >90%.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `alembic/versions/0007_git_sources_kind.py` chains off 0006's real revision id; up/down reversible.
|
||||
- [ ] `uv run alembic upgrade head` applies cleanly; existing rows read `kind='git'`, `path=NULL`.
|
||||
- [ ] The 0007 up/down integration test passes; full `uv run pytest` green; `uv run ruff check . && uv run pyright` clean.
|
||||
@@ -1,27 +0,0 @@
|
||||
# Task 02 — The admin API: the local kind
|
||||
|
||||
**Phase:** `38_local_directory_sources` · **Source:** `TODO.md:11 — "Also need a way to import from existing directory if it's not a git repo"`
|
||||
**Story:** `.agent/user_stories/local-directory-sources.md`
|
||||
|
||||
## Objective
|
||||
Extend the phase-35 git-sources CRUD to accept and return `kind=local` rows with fail-loud path validation, keeping the git contract byte-identical.
|
||||
|
||||
## Work
|
||||
1. The phase-35 API module (the file behind `/api/git-sources` — identify it in the repo; phase 35 created it):
|
||||
- `POST /api/git-sources` body: `{kind?: "git"|"local" (default "git"), url?, path?}`:
|
||||
- `kind=git` → exactly today's `url` validation (trimmed, 1–500 chars, the `https?://` / `ssh://` / `git@` shape, 409 on duplicate without echoing the URL).
|
||||
- `kind=local` → `path` required: trimmed; `Path(p).expanduser()`; must be **absolute after expansion** and an **existing directory on the server** → else `422 {detail: "local source path is not a directory: <path>"}` (a missing path is a user error — fail loud at add-time so the owner sees it immediately; the path is not a secret, so echo it); 409 on a duplicate `path` (detail may name the path).
|
||||
- Wrong field combinations (git without url, local without path, both kinds' fields) → 422.
|
||||
- `GET /api/git-sources` rows gain `kind` + `path` (git rows: `path: null`; the env-fallback rows report `kind: "git"`; `from_env: true` semantics unchanged — env rows are git-only).
|
||||
- `DELETE /api/git-sources/{id}` — unchanged (removal prunes on the next sync, as today).
|
||||
2. The phase-35 request/response models (wherever they live — `app/schemas.py` or the module) — extend for the new fields; keep the OpenAPI docs accurate.
|
||||
3. Integration tests (extend `tests/integration/test_git_sources_api.py`): anonymous → 403 on all routes (regression); `kind=local` + an existing temp dir → 201 with the stored row (`kind=local`, `path` stored expanded); a relative path → 422; a missing path → 422 naming the path; a duplicate path → 409; a git row still validates exactly as before (regression); GET mixes kinds in added order.
|
||||
|
||||
## Testing & Quality
|
||||
- Integration: the matrix above; the existing git-kind suite green unchanged.
|
||||
- Coverage: **>90%** on the modified module.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] The local-kind POST matrix (201 / 422 / 409) green; the git contract unchanged.
|
||||
- [ ] GET rows carry `kind` + `path`; the env fallback stays git-only.
|
||||
- [ ] Full `uv run pytest` green; `uv run ruff check . && uv run pyright` clean.
|
||||
@@ -1,30 +0,0 @@
|
||||
# Task 03 — Sync + `import_docs`: git and local together
|
||||
|
||||
**Phase:** `38_local_directory_sources` · **Source:** `TODO.md:11 — "Also need a way to import from existing directory if it's not a git repo"`
|
||||
**Story:** `.agent/user_stories/local-directory-sources.md`
|
||||
|
||||
## Objective
|
||||
The canonical mirror action (the Sync button) and the CLI import DB **git + local** rows in one run: git rows clone/pull as today, local rows are walked directly; a missing local directory aborts the run loudly before anything is imported.
|
||||
|
||||
## Work
|
||||
1. The phase-35 resolution module (`app/rag/git_sources.py` or wherever `effective_git_sources()` lives — identify it in the repo) — extend:
|
||||
- `effective_sources(db) -> list[GitSource]` — the DB rows of **both** kinds (DB-wins / env-fallback semantics unchanged: while the table is empty, the env git URLs surface as synthetic `kind='git'` rows with `from_env`). Keep a backward-compatible alias (`effective_git_sources`) if other modules import the old name.
|
||||
- Log the list origin as today (`origin=db|env`) plus the kind counts (`git=N local=M`).
|
||||
2. `app/api/sync.py::_run_sync`:
|
||||
- For each resolved row: `kind=git` → `clone_or_pull(url, sources_root / repo_name(url))` (unchanged); `kind=local` → `Path(row.path).expanduser()`, verify `.is_dir()` **at sync time** (the directory may have moved/deleted since add-time) → else raise a sync error `local source missing: <path>` (no credentials involved, but run it through the existing `_sanitize_error` for consistency).
|
||||
- `import_sources(combined, llm, prune=True)` over the **single combined list** (git checkouts + local dirs) — pruning covers the union (phase-32 semantics); the overview regeneration is unchanged (change-gated).
|
||||
- The fail-loud empty-config check: no git rows, no local rows, and no env URLs → `"no sources configured (git or local)"` (replaces phase 32's git-only message).
|
||||
3. `scripts/import_docs.py` — `_resolve_sources` gains the DB path (after `--source`, which still wins over everything): no `--source` and the DB table non-empty → the combined list (git cloned/pulled + local direct); table empty → env git URLs (as today) → the legacy `DEFAULT_SOURCES`. A missing local dir → abort with the path named, before importing anything (the same pre-import fail-loud as a failing git clone).
|
||||
4. Unit/integration:
|
||||
- Unit: `effective_sources` — mixed kinds, DB-wins, env fallback git-only, both-empty (extend the phase-35 tests).
|
||||
- Integration (the `test_sync_api.py` pattern, with a **temp local dir** — a host temp dir containing one fixture `.md`, since the app server runs on the same host): local-only, git-only, and mixed syncs — the local file lands in the KB (`GET /api/docs` as admin); a missing local path → status `failed` with `local source missing: …` in the sanitized error; `import_docs` (no `--source`) with a DB local row imports it; `--source` still wins over the DB.
|
||||
|
||||
## Testing & Quality
|
||||
- The suites above; `test_git_sources_api.py`, `test_sync_api.py`, `test_import_docs_git.py` (phase 35's list) stay green through the indirection.
|
||||
- Coverage: **>90%** on the modified modules.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] A mixed git + local sync imports both in one run; union pruning works (a file deleted from the local dir is pruned on the next sync).
|
||||
- [ ] A missing local dir fails the run loudly (the status error names the path) and imports nothing.
|
||||
- [ ] `import_docs`: DB git + local resolution; `--source` wins; the env fallback is git-only; the both-empty message is updated.
|
||||
- [ ] Full `uv run pytest` green; coverage gate holds; `uv run ruff check . && uv run pyright` clean.
|
||||
@@ -1,24 +0,0 @@
|
||||
# Task 04 — The page: the local-directory form + badges
|
||||
|
||||
**Phase:** `38_local_directory_sources` · **Source:** `TODO.md:11 — "Also need a way to import from existing directory if it's not a git repo"`
|
||||
**Story:** `.agent/user_stories/local-directory-sources.md`
|
||||
|
||||
## Objective
|
||||
The sources page (phase 35) can add a **local directory** next to git repos, the list shows which is which, and the hint reflects the combined Sync semantics.
|
||||
|
||||
## Work
|
||||
1. The phase-35 page (the git-sources HTML template + its page script — identify the actual paths in the repo):
|
||||
- A second add form, **"Local directory"**: a labeled text input (placeholder `~/Notes`), an Add button, an inline error slot — the same never-stale-button + inline-error pattern as the git form (PLAN §7.4); on success the form clears and the list re-fetches.
|
||||
- List rows: a kind badge — `Git` / `Local` (a small styled span, distinguishable by color **and** text, not color alone — WCAG) — plus the mono value (git URL as today; the full local path) + added date + Remove (Remove is unchanged — it prunes on the next sync).
|
||||
- The hint text: "Sync clones/pulls the git repos and imports the local directories together (files removed from a source are pruned)."
|
||||
- Anonymous: the sign-in gate unchanged; the admin-only nav link unchanged (phase 35/29).
|
||||
2. `frontend/assets/styles.css` — the badge styles + the second form (reuse the existing form styles; contrast ≥ 4.5:1 in both themes).
|
||||
3. UI Structure Check (AGENTS.md rule 5) before finalizing: landmarks intact, both inputs labeled, focus-visible on the new Add button, the page stays inside the shared layout (no new top-level structure).
|
||||
|
||||
## Testing & Quality
|
||||
- Gated by the story E2E (task 05); no CDN (AGENTS.md rule 6 — no new external tags).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] The admin adds a local directory through the page; the row appears with the Local badge; an invalid path shows the 422 detail inline and the button recovers (never stale).
|
||||
- [ ] The git form + all existing page behavior unchanged.
|
||||
- [ ] `uv run pytest` green (no frontend unit layer — the E2E is the gate); `uv run ruff check . && uv run pyright` clean.
|
||||
Reference in New Issue
Block a user