Add SSE real-time Talk activity, file editor UI, and identity file API

- event_bus.py: in-process asyncio pub/sub (one Queue per SSE client)
- nextcloud_talk.py: publishes nct_message/nct_response events to bus
- chat.py: GET /events SSE endpoint streams Talk activity to browser
- routers/files.py: whitelist-protected GET/PUT for Inara identity .md files
- main.py: register files router
- static/index.html: real-time Talk feed, blue badge on Sessions btn,
  Files modal with preview/edit toggle and Ctrl+S save

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-03-17 21:10:07 -04:00
parent fe854ee534
commit 3455c7a09c
9 changed files with 460 additions and 1 deletions

View File

@@ -8,6 +8,7 @@ from llm_client import complete
from session_logger import log_turn
from session_store import load as load_session, save as save_session, list_all, generate_session_id
from config import settings
import event_bus
router = APIRouter()
@@ -141,6 +142,30 @@ async def replace_history(session_id: str, req: HistoryUpdate) -> dict:
return {"ok": True, "session_id": session_id}
@router.get("/events")
async def sse_events() -> StreamingResponse:
"""Server-sent events stream — pushes real-time Talk activity to the browser."""
async def stream():
q = event_bus.subscribe()
try:
while True:
try:
event = await asyncio.wait_for(q.get(), timeout=20)
yield f"data: {json.dumps(event)}\n\n"
except asyncio.TimeoutError:
yield 'data: {"type":"keepalive"}\n\n'
except (GeneratorExit, asyncio.CancelledError):
pass
finally:
event_bus.unsubscribe(q)
return StreamingResponse(
stream(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
@router.post("/note")
async def add_note(req: NoteRequest) -> dict:
"""Inject a public note into session history so the LLM sees it next turn."""