Skip to content

feat(chat): direct-chat PWA (v1) — sessions, SSE streaming, /chat app - #3

Draft
tariqismail wants to merge 23 commits into
rb81:mainfrom
tariqismail:chat-pwa-upstream
Draft

feat(chat): direct-chat PWA (v1) — sessions, SSE streaming, /chat app#3
tariqismail wants to merge 23 commits into
rb81:mainfrom
tariqismail:chat-pwa-upstream

Conversation

@tariqismail

Copy link
Copy Markdown
Contributor

Why

The email interface is great for most tasks, but there are times you want to work side by side with the assistant or quickly run something by it without composing an email and waiting on the job round-trip. This adds a direct-chat fast path for exactly those moments, while leaving the email/job workflow as the path for heavier work.

Summary

Adds an optional, installable /chat PWA with live streaming responses. Lightweight turns are answered directly; anything that needs real work escalates into the existing workspace-job pipeline and the reply folds back into the conversation.

What's included

Backend (by module boundary)

  • chat_store.pychat_sessions / chat_messages tables + store (migration 003_chat_sessions.sql)
  • chat_responder.py — fast-path streaming responder
  • api.py — SSE session/message endpoints; escalation wiring (create_workspace_job gains a source tag, create_manual_job an optional thread_id) so job replies fold back into chat context
  • config.py / config/agent.yamlagent.chat config block (documented in docs/env-overrides.md)

Frontend (agent/frontend/src/chat/, second Vite entry)

  • Preact /chat app: thread grouping, conversation view, composer, live progress, streaming cursor, markdown rendering, design tokens
  • Installable PWA: manifest, service worker, icons; "Chat" link in the admin nav

Scope — this is v1

Deliberately kept focused. Known gaps, likely follow-ups:

  • No way to archive/delete chats from the UI yet
  • No attachments in chat
  • No stop/cancel from the chat interface — to halt an escalated turn you currently stop the underlying job

Tests

  • Python: chat endpoints, store, responder, escalation source, workspace/manual thread wiring, plus test_config / test_ui_pages additions — 51 passed via pytest.
  • Frontend: node --test suite (stream, threads) — 18 passed.
  • npm run build (vite) builds cleanly.

Notes

  • Additive and self-contained — no changes outside the chat feature and its escalation wiring; the UI is only reachable via the new /chat route + admin link.
  • Generated chat.bundle.* are gitignored and not included (mirrors the existing workspace.bundle.js convention).

tariqismail and others added 23 commits July 23, 2026 12:14
…gress

Includes smoke-test fixes: flex min-width for text truncation, poll
sequence guard against duplicate logs, back button hidden on desktop.

Co-Authored-By: Claude Fable 5 <[email protected]>
…l, retry 409 follow-ups

The backend writes final_response ~15s before the job flips to completed
(wrap-up work). The UI previously stopped polling at the reply and kept
the composer locked on 'Arqis is working' forever. Now: polling continues
until a terminal status, the composer and working indicators clear as soon
as the reply text exists, and follow-ups sent during wrap-up quietly retry
past the API's 409 instead of failing.

Co-Authored-By: Claude Fable 5 <[email protected]>
Implement data model foundation for direct-chat feature: two new Postgres
tables (chat_sessions, chat_messages) via 003_chat_sessions.sql migration,
plus ChatStore class wrapping raw SQL access to them. All 8 methods specified
by Task 5 interface are implemented. Covered by 6 passing unit tests.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Add three new configuration knobs under agent.chat for the direct-chat
fast path: model (string, empty default), max_history_messages (int,
default 20), and rate_limit_per_minute (int, default 20). Environment
variable overrides and documentation included.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Implements the LLM fast-path for direct chat: turns session history +
new user message into a stream of reply events (delta/escalated/done/error).
Makes streamed OpenRouter chat-completion calls with escalate_to_job tool.
Includes full SSE parsing, tool-call accumulation across chunks, and
fallback to non-streamed LlmClient on stream setup failure.

Includes 11 unit tests covering plain text, split tool args, malformed JSON,
SSE parsing, message building, transcript condensing, and fallback path.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Wires Tasks 1-4 (ChatStore, agent.chat config, chat_responder,
create_workspace_job source tagging) together behind three endpoints:
POST /api/chat/sessions/{session_id}/messages (streaming SSE, rate-limited,
escalates to a workspace job when chat_responder signals it), GET
/api/chat/sessions, and GET /api/chat/sessions/{session_id}/messages.
Also folds chat_messages costs into usage_cost_summary()'s lifetime/month
totals alongside task_logs and deep_research_events.
- parseSSE async generator consumes ReadableStream body and yields JSON events
- Correctly handles SSE frames split across multiple stream reads
- Skips blank/comment lines per SSE spec
- streamChatMessage POSTs to /api/chat/sessions/{sessionId}/messages
- Throws ApiError on non-2xx responses, matching existing request() pattern

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Appended listChatSessions() and getChatSessionMessages(sessionId) to
agent/frontend/src/chat/api.js for listing chat sessions and fetching
transcript messages. Both use the existing request() helper, matching
the style of listJobs and pollJob.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Adds mergeConversations(sessions, jobs) to combine chat sessions and job
threads into a unified, activity-sorted conversation list. Includes session
helpers (sessionTitle, sessionProcessing, sessionSnippet, sessionStatus)
mirroring job-thread accessors. Sorts by most-recent activity using each
session's last_message_at or job thread's latest job created_at.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
…reads

New conversations now always go through streamChatMessage (POST
/api/chat/sessions/{id}/messages) instead of the old createJob/sendFollowUp
workspace-job endpoints, which are no longer called from the UI at all.

- Unified thread list merges chat sessions and legacy job threads via
  mergeConversations, sorted newest-activity-first.
- Legacy job threads (metadata.source === "workspace") render read-only:
  full history + live-poll-to-terminal as before, composer replaced with a
  static notice.
- Chat session messages render as plain bubbles (kind: "chat") or job turns
  with progress steps + reply (kind: "job_ref"), reusing the existing
  /api/jobs/{id}/poll loop.
- Single pollingJobId derivation covers both a legacy thread's active job
  and a session's active job_ref, feeding one poll effect instead of two.
- Composer disables only while the latest job_ref in the active session is
  still processing without a final_response (awaitingSessionReply).
- Streaming replies show token-by-token via Bubble's streaming prop while
  accumulating, then settle into a persisted bubble once the transcript is
  refetched.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
…take them for its own

Prefix job final_response content with "[Completed by the full task pipeline]"
when folding it into job_ref history rows, and tell the quick-chat model (via
QUICK_CHAT_INSTRUCTIONS) to treat such prefixed turns as real completed actions
rather than something it said. Fixes a live bug where the tool-less quick-chat
model saw an unmarked assistant turn describing a tool outcome, concluded it
had hallucinated, apologized, and needlessly re-escalated a duplicate job.
Chat already links back to Admin via its panel header — this completes
the round trip so you can jump to /chat from the dashboard too.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant