feat(projects): Threads im Hauptchat verankert (Stefan-Konzept)
Projekte sind benannte Thema-Bündel die voice-gesteuert via Brain-Tools
geöffnet/verlassen werden. Default-Mode bleibt der Hauptthread — Projekte
sind eine optionale Bühne. Anchored-not-replaced: App-Open landet immer
im Hauptchat, Projekte sind nur sichtbar wenn aktiv betreten.
Brain:
- projects.py: CRUD + Fuzzy-Find + Active-State-Pointer
(/shared/config/projects.json + active_project.txt).
- conversation.py: Turn.project_id-Feld + window(project_id) Filter.
- agent.py: 6 Meta-Tools — project_create / _enter / _exit / _list /
_summary / _end. chat() liest aktive Projekt-ID, taggt User+Assistant-
Turns damit, filtert das LLM-Window auf Projekt-Kontext und ergaenzt
den System-Prompt um den aktiven Projekt-Hinweis. touch_project pflegt
last_activity_at + turn_count.
- main.py: REST-Endpoints /projects/{status,list,create,switch,
{id}/end,{id}/archive, PATCH /{id}}.
Bridge + RVS:
- aria_bridge.py: project_changed Event-Propagation Brain → RVS-Broadcast
damit App + Diagnostic ihre Banner refreshen.
- rvs/server.js: project_changed in ALLOWED_TYPES.
App:
- brainApi.ts: Project-Type + 6 API-Methoden.
- ProjectsBrowser.tsx (neue Komponente, ~340 Zeilen): Status-Header,
Hauptchat als Erster-Eintrag, Projekt-Liste mit Aktiv-Marker, Long-Press
zum Editieren, Modals fuer Neu/Edit/End/Archiv.
- ChatScreen.tsx: Banner unterhalb des Status-Bars zeigt aktives Projekt
oder „Hauptchat" — Tap öffnet ProjectsBrowser als Modal. Aktive Projekt-
Info wird bei Mount + bei project_changed-Events refreshed.
- SettingsScreen.tsx: Neue Section 📁 „Projekte" zeigt ProjectsBrowser inline.
Diagnostic:
- Neue Sektion im Brain-Tab mit Liste, Aktiv-Marker, Beenden/Archivieren
pro Zeile, Modal fuer Neu. Lädt automatisch bei Brain-Tab + bei
project_changed-Event-Broadcast.
Was bewusst NICHT drin ist (Folgeschritte):
- Per-Message Filter im Chat-Verlauf (zeigt aktuell alle Bubbles, Banner
zeigt Kontext) — App müsste Chat-History per project_id filtern.
- Files-by-Project Tagging.
- Inline-Collapse-Bloecke im Chat-Verlauf.
- Sub-Projekte (Stefan-Entscheidung: weglassen, „Mama-tauglich").
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,7 @@ import watcher as watcher_mod
|
||||
import background as background_mod
|
||||
import oauth as oauth_mod
|
||||
import seed_rules as seed_rules_mod
|
||||
import projects as projects_mod
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
||||
logger = logging.getLogger("aria-brain")
|
||||
@@ -639,6 +640,76 @@ def chat(body: ChatIn, background: BackgroundTasks):
|
||||
)
|
||||
|
||||
|
||||
# ── Projekte ────────────────────────────────────────────────────────
|
||||
|
||||
@app.get("/projects/status")
|
||||
def projects_status():
|
||||
"""Komplett-Status: aktives Projekt + Liste aller (nicht-archivierten)."""
|
||||
return projects_mod.status()
|
||||
|
||||
|
||||
@app.get("/projects/list")
|
||||
def projects_list(include_archived: bool = False):
|
||||
return {"projects": projects_mod.list_projects(include_archived=include_archived)}
|
||||
|
||||
|
||||
class ProjectCreateBody(BaseModel):
|
||||
name: str
|
||||
description: str = ""
|
||||
|
||||
|
||||
@app.post("/projects/create")
|
||||
def projects_create(body: ProjectCreateBody):
|
||||
try:
|
||||
p = projects_mod.create_project(body.name, body.description)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
return p
|
||||
|
||||
|
||||
class ProjectSwitchBody(BaseModel):
|
||||
project_id: str = ""
|
||||
|
||||
|
||||
@app.post("/projects/switch")
|
||||
def projects_switch(body: ProjectSwitchBody):
|
||||
"""Aktive Projekt-ID setzen. Leerer String → Hauptthread."""
|
||||
if body.project_id:
|
||||
p = projects_mod.get_project(body.project_id)
|
||||
if not p:
|
||||
raise HTTPException(status_code=404, detail=f"Projekt {body.project_id} nicht gefunden")
|
||||
projects_mod.set_active(body.project_id)
|
||||
return projects_mod.status()
|
||||
|
||||
|
||||
@app.post("/projects/{project_id}/end")
|
||||
def projects_end(project_id: str):
|
||||
if not projects_mod.end_project(project_id):
|
||||
raise HTTPException(status_code=404, detail=f"Projekt {project_id} nicht gefunden")
|
||||
return projects_mod.get_project(project_id) or {"id": project_id, "status": "ended"}
|
||||
|
||||
|
||||
@app.post("/projects/{project_id}/archive")
|
||||
def projects_archive(project_id: str):
|
||||
if not projects_mod.archive_project(project_id):
|
||||
raise HTTPException(status_code=404, detail=f"Projekt {project_id} nicht gefunden")
|
||||
return {"id": project_id, "status": "archived"}
|
||||
|
||||
|
||||
class ProjectUpdateBody(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
@app.patch("/projects/{project_id}")
|
||||
def projects_update(project_id: str, body: ProjectUpdateBody):
|
||||
patch = body.dict(exclude_unset=True)
|
||||
p = projects_mod.update_project(project_id, patch)
|
||||
if p is None:
|
||||
raise HTTPException(status_code=404, detail=f"Projekt {project_id} nicht gefunden")
|
||||
return p
|
||||
|
||||
|
||||
@app.get("/conversation/stats")
|
||||
def conversation_stats():
|
||||
return conversation().stats()
|
||||
|
||||
Reference in New Issue
Block a user