Improvements#2
Open
imdt-joaov wants to merge 24 commits into
Open
Conversation
- 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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reasoning toggle por conversa (006ad75)
PUT /conversations/:cid/reasoning; novo contract + code-pointer +reasoning.tsno backend.supports_reasoning).Migração Tailwind v3 → v4 + shadcn (4c73b58)
tailwind.config.ts+postcss.config.jspor config inline novite.config.ts.ModelPicker.tsxremovido (virou submenu dentro doComposerOptionsMenu).Sidebar de conversas, 3 iterações (de d1344ff até 644518b)
ConversationList.tsxà mão consumindoGET/POST/DELETE /conversations, refetch acoplado a new/delete/first-message, estadoboot.cidno App.ConversationListpras primitivasSidebarMenuda shadcn (a0cca9f/7d859f3/f7675bc).useRemoteThreadListRuntimeda assistant-ui.ConversationList.tsxh-fullno root.Spec + tooling
ui-conversation-*reapontados pros novos símbolos; contratos comsection:atualizado..agents/skills/+.mcp.json+skills-lock.jsonadicionam as skills do assistant-ui e o MCP de docs ao harness (ec24b17).