Skip to content

Improvements#2

Open
imdt-joaov wants to merge 24 commits into
TiagoJacobs:trace-conversationsfrom
imdt-joaov:improvements
Open

Improvements#2
imdt-joaov wants to merge 24 commits into
TiagoJacobs:trace-conversationsfrom
imdt-joaov:improvements

Conversation

@imdt-joaov
Copy link
Copy Markdown

  1. Reasoning toggle por conversa (006ad75)

    • Server expõe PUT /conversations/:cid/reasoning; novo contract + code-pointer + reasoning.ts no backend.
    • Composer ganha o switch "Reasoning" (renderizado só para modelos supports_reasoning).
  2. Migração Tailwind v3 → v4 + shadcn (4c73b58)

    • Substitui o tailwind.config.ts + postcss.config.js por config inline no vite.config.ts.
    • Instala primitivas shadcn (button, card, collapsible, dropdown-menu, input, separator, sheet, skeleton, switch, textarea, tooltip) com tokens Tagus.
    • Composer unificado, ModelPicker.tsx removido (virou submenu dentro do ComposerOptionsMenu).
  3. Sidebar de conversas, 3 iterações (de d1344ff até 644518b)

    • v1: ConversationList.tsx à mão consumindo GET/POST/DELETE /conversations, refetch acoplado a new/delete/first-message, estado boot.cid no App.
    • v2: porta o ConversationList pras primitivas SidebarMenu da shadcn (a0cca9f/7d859f3/f7675bc).
    • v3 (final): troca tudo por useRemoteThreadListRuntime da assistant-ui. ConversationList.tsx
    • Cosmético junto: emojis → Lucide (Wrench/Zap/Brain/etc.), scrollbar fino + h-full no root.
  4. Spec + tooling

    • 4 code-pointers ui-conversation-* reapontados pros novos símbolos; contratos com section: atualizado.
    • ADR-0009 e o code-pointer de delete com PENDING RECONCILIATION (Radix entrou indireto via shadcn; auto-recreate ao deletar a thread ativa migrou pro runtime e falta verificar end-to-end).
    • .agents/skills/ + .mcp.json + skills-lock.json adicionam as skills do assistant-ui e o MCP de docs ao harness (ec24b17).

- Tailwind v3 (PostCSS + config) -> v4 (@tailwindcss/vite, CSS-only @theme).
- shadcn/ui installed (radix-nova style, neutral baseColor, CSS variables).
- ModelPicker + ReasoningToggle merged into a single ComposerOptionsMenu
  dropdown (model picker + reasoning toggle in one menu).
- Tagus design tokens applied to index.css: primary #149FD9,
  secondary #edf5f8, semantic colors, Poppins; light + dark.
- Composer: ComposerPrimitive.Root carries the input chrome; the textarea
  is reduced to a bare element so input + controls read as one surface.
- Root and ui/ lockfiles migrated from npm to bun.
Lift jwtRef and authedFetch up to App so the upcoming sidebar can issue
authed requests at the same scope as the chat runtime. Replace the
one-shot boot state with newConversation / switchConversation /
deleteConversation actions and a new lib/authedFetch.ts factory shared
by both layers; ChatRoom now receives the jwtRef and authedFetch as
props instead of owning them. No UX change yet — the sidebar consumer
lands in a later step.
Stand-alone sidebar content: fetches GET /conversations on mount and
whenever refetchKey changes, renders the header (Conversas + "+ Nova"),
a separator, and one row per conversation with title fallback, a
relative-time stamp, and a hover-revealed delete control. The component
is not yet wired into App.tsx; that lands in a later step.
Introduce a refetchKey counter in App, bumped by newConversation,
deleteConversation, and the first-user-message transition inside
ChatRoom. The first-message trigger uses runtime.thread.subscribe with
a per-mount ref so it fires exactly once per conversation (0→≥1
messages.length transition) — the server-derived title appears in the
sidebar as soon as the round-trip lands. The consumer (the sidebar
itself) is still wired in the next step.
Wire the ConversationList into a fixed-width aside on the left, with
the chat column flexing to the right (min-w-0 so long code blocks
inside messages cannot blow out the layout). The demo banner now lives
in the chat column so the sidebar palette stays untainted by the
destructive tint.
Replace the placeholder text states with: three pulsing skeleton rows
during the initial fetch, a centered "Nenhuma conversa ainda." copy
when the list comes back empty, and an error row with a Reconectar
button that retriggers the fetch via an internal attempt counter.
Active row carries aria-current="page" for screen readers.
Per /code-changed (Case A — code and spec agree). The UI sidebar
landed in commits d1344ff..87a4d97 and is a new consumer of four
already-current contracts: GET /conversations, POST /conversations,
DELETE /conversations/:cid, and GET /conversations/:cid/messages.
Adds one code-pointer per consumer under spec/src/evidence/code-pointers/
and references them from each contract's evidence list. No status
promotions — the contracts were already current; no UI tests in this
change.

session-chat.md is intentionally left alone: its mentions of the
"bundled UI" are scoped to chat-turn rendering (model chip, reasoning
section, read-only banner), not the conversation list. The sidebar
belongs to the four contracts above.
Add sidebar block (collapsible="icon" mode + SidebarProvider/SidebarInset
plumbing) and its block dependencies (sheet, skeleton, use-mobile hook,
input). The sidebar component lands unused; consumers in App.tsx and
ConversationList.tsx switch over in the next two commits. Tooltip gained
a "use client" header to match the other primitives — no behavior change
in a Vite build.
Swap the custom <aside> + flex column wrapper for shadcn's
SidebarProvider + Sidebar (collapsible="icon") + SidebarInset. Adds a
thin header at the top of the inset hosting SidebarTrigger so users can
collapse the sidebar to its icon rail. The demo banner and ChatRoom keep
their relative order — banner pinned at the top by flex order, composer
pinned at the bottom by ChatRoom's internal flex column, no
position:sticky needed. ConversationList still renders the legacy markup
inside Sidebar; the next commit swaps it to SidebarMenu primitives.
Replace the hand-rolled <button>/<span role="button"> row layout with
SidebarHeader + SidebarContent + SidebarMenu + SidebarMenuItem +
SidebarMenuButton + SidebarMenuAction. Three concrete wins:

- The ✕ delete control is now a real <button> via SidebarMenuAction
  instead of <span role="button">, fixing the nested-interactive HTML
  violation we previously had to dance around.
- "+ Nova" lives in SidebarHeader and collapses to an icon-only button
  in icon-rail mode; row tooltips kick in automatically in that mode.
- Loading skeletons use the shadcn SidebarMenuSkeleton primitive.

Also patches sidebar.tsx so data-active is omitted when isActive is
false. Tailwind v4's data-active: modifier matches attribute presence,
not truthiness (shadcn-ui/ui#9134), so passing isActive={false} would
otherwise render every row in the active style.
After the sidebar refactor (a0cca9f..f7675bc), the four UI consumers
moved: ConversationList.tsx grew the shadcn primitives wrapper and
App.tsx now lives inside SidebarProvider/SidebarInset. Bump each
code-pointer's ref to f7675bc so the evidence still resolves. No
content change — the contracts these prove (GET/POST/DELETE
/conversations, GET /conversations/:cid/messages) are unaffected by
the layout reshuffle.
Replaces the bespoke sidebar (ConversationList.tsx + App-level
new/switch/delete callbacks) with assistant-ui's RemoteThreadList:

- install the assistant-ui shadcn registry + threadlist-sidebar and
  thread-list components (header trimmed to a content-only shell,
  Archive item removed)
- new createThreadListAdapter mapping the contract to /conversations
  REST (list, initialize, delete, fetch, generateTitle); rename /
  archive / unarchive stay as no-ops since the server has no
  matching endpoints
- new ThreadHistoryAdapter via withFormat(aiSDKV6FormatAdapter) so
  GET /conversations/:cid/messages hydrates each thread's chat
  runtime; append is a no-op (the /chat stream persists server-side)
- App.tsx rewritten around useRemoteThreadListRuntime; runtimeHook
  eagerly initialize()s the active thread so /chat body.id is
  populated before the user types
- URL /c/<cid> is mirrored from the active thread's remoteId via a
  small UrlSync effect; deep-links use initialThreadId
- Composer reads conversationId from useAuiState so ConnectorsMenu /
  ComposerOptionsMenu stay tied to the active thread
Swap 🛠 ⚡ ⚖ 🧠 🔧 🔗 📄 ▾ for Wrench / Zap / Scale / Brain /
Wrench / Link2 / FileText / ChevronDown in the composer menus,
tool-call blocks, and source chips. App-level swaps (Bot for the
ModelChip and ChevronLeft/Right for BranchPicker) already landed
with the thread-list refactor.
Adds a slim webkit/firefox scrollbar style and stretches html /
body / #root to 100% height so the SidebarProvider's h-full
sizing reaches the viewport instead of collapsing to content.
Rewrites the four ui-conversation-* code-pointers to point at the
new entry points (createThreadListAdapter / createHistoryAdapter /
ThreadListPrimitive triggers) and updates the contracts' section
labels to match. The delete pointer carries a PENDING
RECONCILIATION block: the old "if active, mint a fresh one" branch
is now owned by useRemoteThreadListRuntime and hasn't been
verified end-to-end.

ADR-0009 also gets a PENDING RECONCILIATION block: the post-impl
"No Radix / no wrappers" claim no longer holds — the assistant-ui
shadcn registry components wrap Sidebar / Button / Skeleton, which
are Radix-based. Proposed direction: amend the bullet or spin a
new ADR; left for human review.

@wip markers in the code-pointer refs are placeholders until this
branch merges; bump to the merge commit then.
AUGCHATD_JWT_SECRET is now sourced from env: required in prod (length >= 32,
no placeholders), optional in demo where an ephemeral random secret is
generated at boot with a console warning that flags the implication
(every restart invalidates open sessions). The IIFE module-level secret
in src/jwt.ts is replaced by initJwt() wired from src/index.ts.

The iframe postMessage handshake now learns the parent origin from a
?parent_origin= query string on its own src URL and uses that as the
strict comparison value for inbound augchatd:jwt and as the targetOrigin
for outbound augchatd:ready / augchatd:route. When the query param is
absent, the iframe degrades to document.referrer's origin and logs a
one-time console.warn (back-compat for embedders that pre-date this
contract). The demo wrapper auto-appends the query param so /demo/
exercises the strict path daily.

Spec contracts updated to close the "iframe-side origin discovery"
known gap: browser-postmessage now documents the ?parent_origin= +
degrade mechanism, and ui-handshake's pending entry is removed.
…leware

augchatd no longer aspires to terminate TLS in-process. Bun.serve's TLS
config cannot expose the peer client certificate to a request handler
today (oven-sh/bun#12822, oven-sh/bun#16254), so the mTLS handshake moves
to a reverse proxy that validates the cert and forwards two headers:

  X-Client-Cert-Verify:  SUCCESS
  X-Client-Cert-Subject: CN=alice,OU=eng,O=acme   (RFC 2253)

New modules:
  - src/mtls-trust.ts — requireMtlsTrust middleware: gates on Verify ==
    "SUCCESS" and parses the Subject DN into a lowercased attribute map.
  - src/identity.ts — requireIdentity middleware: maps O -> tenantId,
    CN -> userId, validates the same alphabet src/env.ts uses for
    filesystem-bound idents.

New env var TRUSTED_PROXY: the operator's explicit declaration that
augchatd is reachable only via the proxy (loopback, unix socket, or
private network). In prod without it, mTLS routes are not mounted and a
boot warning is emitted. The header trust is a declarative agreement —
the flag does not verify the call's network origin, only the operator's
intent.

Wiring of POST /sessions and DELETE /sessions/:id onto these middlewares
is deferred to PR C / PR D; this PR is the architectural prep plus the
sample nginx config, ADR-0012, and updates to components.md,
security.md, and .env.local.example.

Smoke-verified the middleware chain in isolation (Hono app.request
against the composed middleware): no headers -> 401, Verify=FAILED ->
401, missing subject -> 401, malformed subject -> 400, subject without
O/CN -> 400, subject with invalid alphabet -> 400, valid subject -> 200
with parsed identity.
mcp.ts and rag.ts no longer hold module-level state. Their client maps
(ConnectedMcp / ConnectedRag) and the rag hits-by-toolCallId map move
into SessionRecord, so credentials and intermediate results stay inside
the session boundary. Signatures change accordingly: the caller passes
the session's Map into init/dispatch functions instead of relying on a
hidden singleton.

SessionRecord gains SessionConnectorState (mcpClients, ragClients,
ragHitsByToolCall). bindDemoSession takes the boot-shared Maps by
reference (demo is single-tenant, single-user — connecting to MCP on
every /demo/sessions mint would tank the JWT-refresh path); bindSession
(new) allocates fresh Maps per session for the prod path. unregisterSession
is added in preparation for DELETE /sessions/:id (PR D).

New POST /sessions handler at src/routes/sessions.ts: validates body via
zod, runs the LLM-credential probe and S3-writability probe (same posture
as demo boot), allocates the SessionRecord via bindSession, then runs
initMcpConnectors / initRagConnectors against the session's own Maps,
finally mintJwt and return. server.ts mounts this route — plus the
JWT-bearer chat/conversation/model routes — only when
`mode=prod && trusted_proxy`. The body's user_id is authoritative; the
cert's CN is sanity-checked but the integrator is the source of truth
for which user this session is for. tenantId comes from the cert's O.

Smoke-tested in-process: no mTLS -> 401 mtls_required, missing fields ->
400 invalid_payload with per-field zod detail, bad LLM key -> 400
llm_credential_probe_failed. Demo boot unchanged (MCP/RAG still connect
once at boot; bindDemoSession shares the boot-initialized Maps with
every minted demo session).
Mounts the mTLS-gated DELETE handler that fulfills contract-session-delete.
SessionRecord gains an AbortController; chat.ts merges it with the
per-request signal via AbortSignal.any, so a forced delete interrupts
the in-flight LLM stream and its tool calls immediately. After the
abort, the handler:

  1. Yields a microtask so the chat handler's onFinish callback lands
     the partial assistant message into hot SQLite before serialization.
  2. flushAllForSession (new in flush-scheduler.ts) — synchronously
     drains the per-(tenant, user) flush states. Now returns a boolean:
     true iff every targeted conversation is cleanlyFlushed after the
     attempt.
  3. If allFlushed is false: 503 with `flush_failed`, session stays
     registered for retry (per contract-session-delete "5xx if the
     final flush to cold cannot be confirmed; the session is not
     released — the integrator may retry").
  4. closeMcpClients — calls Transport.close() on each per-session MCP
     transport. RAG has no socket lifecycle.
  5. unregisterSession + noteSessionEnd — may trigger hot eviction via
     the existing maybeEvict path.

Cross-tenant DELETE → 403 (cert's O must match session.tenant_id).
Unknown session id → 404 (matches the spec's at-most-once semantics:
first DELETE returns 204, second returns 404).

Spec sync: removed PENDING RECONCILIATION block on session-delete.md
(the route is now mounted with the contract's promised behavior),
added evidence pointers on both session-delete and http-delete-sessions,
linked them to adr-0012.

Smoke-tested in-process: missing mTLS -> 401, wrong tenant -> 403,
unknown id -> 404, valid delete -> 204 with session removed and abort
signal fired, repeat -> 404 (idempotent at the result level, not the
status-code level).
@imdt-joaov imdt-joaov marked this pull request as ready for review May 28, 2026 12:32
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.

2 participants