Skip to content

Commit f934871

Browse files
authored
Merge pull request #24 from ossirytk/vibe-web-page
Web ui work
2 parents 2d84cd9 + b474153 commit f934871

26 files changed

Lines changed: 2801 additions & 19 deletions

.github/copilot-instructions.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,49 @@ All terminal commands should be reproducible from the supported shell/editor com
2525

2626
---
2727

28+
## 0.1 Available CLI Tools
29+
30+
The following tools are installed locally and available for use in terminal workflows and agent tasks:
31+
32+
| Tool | Purpose |
33+
|------|---------|
34+
| `diffutils` | File comparison (`diff`, `cmp`, `diff3`, `sdiff`) |
35+
| `fd` | Fast, user-friendly alternative to `find` for file search |
36+
| `fzf` | General-purpose fuzzy finder for interactive filtering |
37+
| `ripgrep` (`rg`) | Fast regex search across files; prefer over `grep`/`Select-String` |
38+
| `zip` | Archive creation and extraction |
39+
| `tokei` | Count lines of code by language |
40+
| `ast-grep` (`sg`) | Structural code search and rewriting using AST patterns |
41+
| `jq` | JSON query and transformation CLI |
42+
| `yq` | YAML/JSON/TOML query and transformation CLI |
43+
| `hyperfine` | Command-line benchmarking with statistical output |
44+
| `pre-commit` | Run and manage repository pre-commit hooks |
45+
| `http` / `https` (HTTPie) | Human-friendly HTTP API client |
46+
| `just` | Project task runner via `justfile` recipes |
47+
| `difft` (difftastic) | Syntax-aware structural diffing |
48+
49+
Prefer these tools over PowerShell built-ins where applicable (e.g., use `rg` instead of `Select-String`, use `fd` instead of `Get-ChildItem` for file discovery).
50+
51+
### Preferred command order
52+
53+
- Content search: `rg` first, then `ast-grep` for structural/language-aware matching
54+
- File discovery: `fd` first, then `rg --files` as a fallback
55+
- JSON config inspection: `jq`
56+
- YAML/TOML inspection: `yq`
57+
- HTTP/API smoke checks: `http` / `https` (HTTPie)
58+
- Task orchestration: `just` recipes when a `justfile` exists
59+
- Diff/review: `difft` for syntax-aware diffs, `diff` for plain text diffs
60+
- Performance comparisons: `hyperfine` for repeatable timing
61+
62+
### Avoid in autonomous runs
63+
64+
- Avoid interactive-only flows (for example `fzf` prompts) unless the user explicitly asks for interactive selection
65+
- Avoid destructive git/file operations unless the user explicitly approves them
66+
- Avoid long-running watch commands by default; use one-shot checks first, then switch to watch mode only when requested
67+
- Avoid invoking `pre-commit run --all-files` on very large repos when a targeted path or hook is enough for the task
68+
69+
---
70+
2871
## 1. Authoritative Tools & Source of Truth
2972

3073
### Python

AGENTS.MD

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,49 @@ All terminal commands should be reproducible from the supported shell/editor com
2525

2626
---
2727

28+
## 0.1 Available CLI Tools
29+
30+
The following tools are installed locally and available for use in terminal workflows and agent tasks:
31+
32+
| Tool | Purpose |
33+
|------|---------|
34+
| `diffutils` | File comparison (`diff`, `cmp`, `diff3`, `sdiff`) |
35+
| `fd` | Fast, user-friendly alternative to `find` for file search |
36+
| `fzf` | General-purpose fuzzy finder for interactive filtering |
37+
| `ripgrep` (`rg`) | Fast regex search across files; prefer over `grep`/`Select-String` |
38+
| `zip` | Archive creation and extraction |
39+
| `tokei` | Count lines of code by language |
40+
| `ast-grep` (`sg`) | Structural code search and rewriting using AST patterns |
41+
| `jq` | JSON query and transformation CLI |
42+
| `yq` | YAML/JSON/TOML query and transformation CLI |
43+
| `hyperfine` | Command-line benchmarking with statistical output |
44+
| `pre-commit` | Run and manage repository pre-commit hooks |
45+
| `http` / `https` (HTTPie) | Human-friendly HTTP API client |
46+
| `just` | Project task runner via `justfile` recipes |
47+
| `difft` (difftastic) | Syntax-aware structural diffing |
48+
49+
Prefer these tools over PowerShell built-ins where applicable (e.g., use `rg` instead of `Select-String`, use `fd` instead of `Get-ChildItem` for file discovery).
50+
51+
### Preferred command order
52+
53+
- Content search: `rg` first, then `ast-grep` for structural/language-aware matching
54+
- File discovery: `fd` first, then `rg --files` as a fallback
55+
- JSON config inspection: `jq`
56+
- YAML/TOML inspection: `yq`
57+
- HTTP/API smoke checks: `http` / `https` (HTTPie)
58+
- Task orchestration: `just` recipes when a `justfile` exists
59+
- Diff/review: `difft` for syntax-aware diffs, `diff` for plain text diffs
60+
- Performance comparisons: `hyperfine` for repeatable timing
61+
62+
### Avoid in autonomous runs
63+
64+
- Avoid interactive-only flows (for example `fzf` prompts) unless the user explicitly asks for interactive selection
65+
- Avoid destructive git/file operations unless the user explicitly approves them
66+
- Avoid long-running watch commands by default; use one-shot checks first, then switch to watch mode only when requested
67+
- Avoid invoking `pre-commit run --all-files` on very large repos when a targeted path or hook is enough for the task
68+
69+
---
70+
2871
## 1. Authoritative Tools & Source of Truth
2972

3073
### Python

core/job_queue.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Simple in-memory job store for long-running RAG web operations.
2+
3+
Jobs run in background threads. Route handlers poll for status via HTMX
4+
(`hx-trigger="every 2s"`). The job status endpoint stops including the
5+
polling trigger once the job reaches a terminal state (done or error).
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import threading
11+
import time
12+
import uuid
13+
from typing import TYPE_CHECKING, Any
14+
15+
if TYPE_CHECKING:
16+
from collections.abc import Callable
17+
18+
19+
class Job:
20+
"""In-memory representation of a background job."""
21+
22+
__slots__ = ("error", "finished_at", "id", "result", "started_at", "status")
23+
24+
def __init__(self, job_id: str) -> None:
25+
self.id = job_id
26+
self.status: str = "pending"
27+
self.result: Any = None
28+
self.error: str | None = None
29+
self.started_at: float = time.monotonic()
30+
self.finished_at: float | None = None
31+
32+
def to_dict(self) -> dict[str, Any]:
33+
elapsed = round((self.finished_at or time.monotonic()) - self.started_at, 2)
34+
return {
35+
"id": self.id,
36+
"status": self.status,
37+
"result": self.result,
38+
"error": self.error,
39+
"elapsed_s": elapsed,
40+
}
41+
42+
43+
class JobStore:
44+
"""Thread-safe store for background jobs."""
45+
46+
MAX_JOBS: int = 50
47+
48+
def __init__(self) -> None:
49+
self._jobs: dict[str, Job] = {}
50+
self._lock = threading.Lock()
51+
52+
def submit(self, fn: Callable[..., Any], *args: object, **kwargs: object) -> str:
53+
"""Submit a callable as a background job; returns a job_id immediately."""
54+
job_id = uuid.uuid4().hex[:12]
55+
job = Job(job_id)
56+
with self._lock:
57+
self._evict_old()
58+
if len(self._jobs) >= self.MAX_JOBS:
59+
msg = f"Job store is full ({self.MAX_JOBS} active jobs); please wait for a job to finish"
60+
raise RuntimeError(msg)
61+
self._jobs[job_id] = job
62+
63+
def _run() -> None:
64+
job.status = "running"
65+
try:
66+
job.result = fn(*args, **kwargs)
67+
job.status = "done"
68+
except Exception as exc:
69+
job.error = str(exc)
70+
job.status = "error"
71+
finally:
72+
job.finished_at = time.monotonic()
73+
74+
threading.Thread(target=_run, daemon=True).start()
75+
return job_id
76+
77+
def get(self, job_id: str) -> dict[str, Any] | None:
78+
"""Return job state dict, or None if job_id is unknown."""
79+
with self._lock:
80+
job = self._jobs.get(job_id)
81+
return job.to_dict() if job else None
82+
83+
def _evict_old(self) -> None:
84+
"""Remove oldest finished jobs when over the cap (called under lock)."""
85+
if len(self._jobs) <= self.MAX_JOBS:
86+
return
87+
finished = [j for j in self._jobs.values() if j.status in {"done", "error"}]
88+
finished.sort(key=lambda j: j.finished_at or 0)
89+
for j in finished[: len(self._jobs) - self.MAX_JOBS]:
90+
del self._jobs[j.id]

core/preset_profiles.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""Saveable preset profiles for runtime retrieval settings."""
2+
3+
from __future__ import annotations
4+
5+
import dataclasses
6+
import json
7+
from typing import TYPE_CHECKING
8+
9+
if TYPE_CHECKING:
10+
from pathlib import Path
11+
12+
from core.config import ConversationRuntimeConfig
13+
14+
PROFILE_FIELDS: list[str] = [
15+
"use_mmr",
16+
"rag_rerank_enabled",
17+
"rag_sentence_compression_enabled",
18+
"rag_multi_query_enabled",
19+
"rag_k",
20+
"rag_k_mes",
21+
"debug_context",
22+
]
23+
24+
25+
class ProfileStore:
26+
"""Persist and apply named retrieval-setting presets stored in a JSON file."""
27+
28+
def __init__(self, path: Path) -> None:
29+
self._path = path
30+
31+
def _load(self) -> dict[str, dict[str, object]]:
32+
if not self._path.exists():
33+
return {}
34+
try:
35+
data = json.loads(self._path.read_text(encoding="utf-8"))
36+
return data if isinstance(data, dict) else {}
37+
except Exception:
38+
return {}
39+
40+
def _save(self, data: dict[str, dict[str, object]]) -> None:
41+
self._path.parent.mkdir(parents=True, exist_ok=True)
42+
self._path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
43+
44+
def list_profiles(self) -> list[str]:
45+
"""Return sorted list of saved profile names."""
46+
return sorted(self._load().keys())
47+
48+
def save_profile(self, name: str, config: ConversationRuntimeConfig) -> None:
49+
"""Snapshot the profile-eligible fields from *config* under *name*."""
50+
data = self._load()
51+
data[name] = {field: getattr(config, field) for field in PROFILE_FIELDS}
52+
self._save(data)
53+
54+
def get_profile(self, name: str) -> dict[str, object]:
55+
"""Return the stored settings dict for *name*."""
56+
data = self._load()
57+
if name not in data:
58+
msg = f"Profile {name!r} not found"
59+
raise KeyError(msg)
60+
return dict(data[name])
61+
62+
def apply_profile(
63+
self, name: str, config: ConversationRuntimeConfig
64+
) -> tuple[ConversationRuntimeConfig, list[str]]:
65+
"""Return a new config with profile values applied and list of changed field names."""
66+
profile = self.get_profile(name)
67+
validated_updates: dict[str, object] = {}
68+
changed: list[str] = []
69+
for field, value in profile.items():
70+
if field not in PROFILE_FIELDS:
71+
continue
72+
current = getattr(config, field, None)
73+
if current != value:
74+
validated_updates[field] = value
75+
changed.append(field)
76+
if validated_updates:
77+
config = dataclasses.replace(config, **validated_updates)
78+
return config, changed
79+
80+
def delete_profile(self, name: str) -> None:
81+
"""Remove *name* from the store (no-op if not found)."""
82+
data = self._load()
83+
data.pop(name, None)
84+
self._save(data)
85+
86+
def current_values(self, config: ConversationRuntimeConfig) -> dict[str, object]:
87+
"""Return current values of the profile-eligible fields from *config*."""
88+
return {field: getattr(config, field) for field in PROFILE_FIELDS}

0 commit comments

Comments
 (0)