fabro/apps/fabro-web/app/app.css
Bryan Helmkamp 7ac15b28f0
feat(fabro-web): port /chats/new + /chats/:id from prototype (#289)
## Summary

Ports the validated `/chats/new` and `/chats/:id` chat surface from
`docs/superpowers/prototypes/2026-05-16-chats-new/` into
`apps/fabro-web`. Client-side scripted prototype mounted inside the
existing `AppShell`; replaces `/start` as the planned new "kick off
agent work" entry point (but does not delete `/start` in this phase).

- New routes: `/chats/new` (empty-state composer) and `/chats/:chatId`
(active conversation with assistant-ui's `<Thread>`, scripted streaming
replies, markdown + tool-call rendering).
- Drives `@assistant-ui/react` + `@assistant-ui/react-ui` via
`useLocalRuntime` and a custom `ChatModelAdapter` that cycles a 6-entry
scripted reply bank.
- Tailwind v4 cascade fix: assistant-ui CSS is now imported via `@layer
assistant-ui` so v4 utilities cascade above the package's unlayered
scoped preflight. Includes a discovered Bun-specific tweak — see Notable
Deviations below.
- StrictMode-safe first-message handoff: store seeds the user message
into `seedMessages` with a `pendingResponse: true` flag, and
`chats-detail` triggers a single `runtime.thread.startRun({ parentId:
null })` then consumes the flag. Avoids the prototype's
autorespond-lost-stream race under React 19 StrictMode.

The Ask-Fabro right sidebar (also in the prototype) is **out of scope**
for this PR.

Companion spec:
[`docs/superpowers/specs/2026-05-16-chats-new-prototype-design.md`](../tree/chats-new-port/docs/superpowers/specs/2026-05-16-chats-new-prototype-design.md)
Implementation plan:
[`docs/superpowers/plans/2026-05-16-chats-new-fabro-web-port.md`](../tree/chats-new-port/docs/superpowers/plans/2026-05-16-chats-new-fabro-web-port.md)

## Screenshots

Captured from a local debug `fabro server` running this branch's binary,
signed in via GitHub.

### `/chats/new` (empty state)

![chats-new empty
state](https://github.com/fabro-sh/fabro/raw/chats-new-port/docs/superpowers/prototypes/2026-05-16-chats-new/screenshots/chats-new-v4.png)

### `/chats/:chatId` (active conversation)

![chats-detail active
chat](https://github.com/fabro-sh/fabro/raw/chats-new-port/docs/superpowers/prototypes/2026-05-16-chats-new/screenshots/chats-detail-v4.png)

## Files

**New** (under `apps/fabro-web/`):
- `app/lib/chats-types.ts` — `Chat` wrapper + `ChatContentPart`
discriminated union over the API client's `CompletionContentPart`
- `app/lib/chats-script.ts` — 6-entry scripted reply bank
(`CompletionMessage[]`)
- `app/lib/chats-store.tsx` — Context + `useReducer` for chat metadata,
`pendingResponse` flag, scriptIndex
- `app/lib/chats-runtime.ts` — `createScriptedAdapter` +
`toThreadMessages` boundary converter
- `app/lib/test-utils.tsx` — minimal `renderHook` shim (lifts the
duplicated `IS_REACT_ACT_ENVIRONMENT` + dep-warning silencing pattern
out of `install-app.test.tsx`)
-
`app/components/chats/{tool-fallback,composer-chips,custom-composer}.tsx`
- `app/routes/{chats-layout,chats-new,chats-detail}.tsx`
- Tests: `chats-store.test.tsx` (5), `chats-runtime.test.ts` (4),
`chats-router.test.tsx` (3)

**Modified:**
- `package.json` — adds `@assistant-ui/{react,react-ui,react-markdown}`
(pinned exactly to versions verified in the prototype)
- `app/app.css` — `@layer` declaration + assistant-ui CSS imports into
`layer(assistant-ui)` + `.fabro-chat` `--aui-*` variable overrides
mapping to the Fabro palette
- `app/root.tsx` — removed `import "./app.css"` (see Notable Deviations)
- `app/router.tsx` — wires the chats routes under the AppShell tree

## Notable deviations from the plan

Two intentional deviations, both explained in their commit bodies:

1. **`apps/fabro-web/app/root.tsx` no longer imports `./app.css`.**
Bun's CSS bundler (used by `Bun.build` on `entry.tsx`) rejects
spec-valid `@layer name, name;` ordering between `@import` rules, even
though Tailwind's CLI accepts it. The CSS is built standalone by the
Tailwind CLI step in `scripts/build.ts` and linked from
`index.template.html`, so dropping the JS-side import bypasses Bun's
parser without any runtime change. A safety-net comment at the top of
`app.css` warns future engineers against re-adding the import. Commit:
`c37690be9`.
2. **`!` non-null assertions removed** in two places where the verbatim
prototype copy violated the global CLAUDE.md rule banning `!` in
production code: `chats-script.ts` now uses a typed `FALLBACK_REPLY` and
`??` coalescing; `composer-chips.tsx` lifts the first option of each
chip into a `DEFAULT_*` constant. `chats-runtime.test.ts`'s `for await`
drain loops were also replaced with `Array.fromAsync(...)` per the
no-loops-in-tests rule. Commits: `ace6ac6d4`, `652ad97af`.

## Test plan

- [x] `cd apps/fabro-web && bun run typecheck` — clean
- [x] `bun test` — 383 pass / 0 fail (12 new tests for chats)
- [x] `cd apps/fabro-web && bun run build` — succeeds; assistant-ui CSS
bundled into `dist/assets/app.css`
- [x] **Manual browser smoke test** — debug `fabro` binary running this
branch served `/chats/new` and `/chats/seed_email` correctly inside the
real AppShell with GitHub-OAuth auth (screenshots above).

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:10:16 -04:00

187 lines
6.1 KiB
CSS

@import "@xterm/xterm/css/xterm.css";
/*
* Do NOT `import "./app.css"` from JS — Bun's CSS bundler rejects the
* `@layer name, name;` ordering rule between `@import` rules below. This file
* is built standalone by the Tailwind CLI step in `scripts/build.ts`; the
* generated `assets/app.css` is linked from `index.template.html`.
*/
/*
* Cascade layer order. @assistant-ui/react-ui ships unlayered CSS authored
* against Tailwind v3; in Tailwind v4 utilities live in @layer utilities, and
* any unlayered CSS wins over layered CSS regardless of selector specificity.
* Putting assistant-ui in a named layer that we declare BEFORE utilities makes
* Tailwind v4 utility classes cascade above assistant-ui's scoped preflight.
*/
@layer theme, base, assistant-ui, components, utilities;
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@import "@assistant-ui/react-ui/styles/index.css" layer(assistant-ui);
@import "@assistant-ui/react-ui/styles/markdown.css" layer(assistant-ui);
@layer base {
a[href],
button,
select,
summary,
[role="button"],
[role="link"],
[role="option"],
[role="menuitem"],
[role="tab"] {
cursor: pointer;
}
}
@theme {
--font-sans: "Geist", ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-mono: "JetBrains Mono", ui-monospace, monospace;
/* Primary — Teal */
--color-teal-300: #B5DDEF;
--color-teal-500: #67B2D7;
--color-teal-700: #357F9E;
/* Neutrals Dark — Navy */
--color-navy-950: #0F1729;
--color-navy-800: #252C3D;
--color-navy-600: #4B5768;
/* Neutrals Light — Ice */
--color-ice-50: #F7F9FB;
--color-ice-100: #E8EDF3;
--color-ice-300: #A8B5C5;
/* Accent */
--color-mint: #5AC8A8;
--color-amber: #F0A45B;
--color-coral: #E86B6B;
/* ── Semantic tokens ── */
--color-page: #0F1729;
--color-panel: #252C3D;
--color-panel-alt: #1a2133;
--color-overlay: rgba(255, 255, 255, 0.04);
--color-overlay-strong: rgba(255, 255, 255, 0.10);
--color-fg: #ffffff;
--color-fg-2: #E8EDF3;
--color-fg-3: #A8B5C5;
--color-fg-muted: #4B5768;
--color-line: rgba(255, 255, 255, 0.06);
--color-line-strong: rgba(255, 255, 255, 0.12);
--color-focus: rgba(103, 178, 215, 0.4);
/* Text color that sits on --color-teal-500 primary fills. */
--color-on-primary: #0F1729;
}
/* ── Chart variables ── */
:root {
--chart-gridline: rgba(255, 255, 255, 0.04);
--chart-axis-label: rgba(75, 87, 104, 1);
}
/* ── Background atmosphere ── */
.bg-atmosphere {
background-color: var(--color-page);
background-image:
radial-gradient(ellipse 120% 60% at 15% 5%, rgba(53, 127, 158, 0.12) 0%, transparent 50%),
radial-gradient(ellipse 80% 50% at 85% 90%, rgba(90, 200, 168, 0.07) 0%, transparent 45%);
background-attachment: fixed;
}
.bg-atmosphere::before {
content: "";
position: fixed;
inset: 0;
z-index: 1;
pointer-events: none;
opacity: 0.07;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
background-repeat: repeat;
background-size: 200px 200px;
}
/* ── @pierre/diffs brand overrides ── */
:root {
--diffs-font-family: "JetBrains Mono", ui-monospace, monospace;
--diffs-header-font-family: "Geist", ui-sans-serif, system-ui, sans-serif;
--diffs-font-size: 13px;
--diffs-line-height: 20px;
/* Surfaces */
--diffs-bg-buffer-override: #0F1729;
--diffs-bg-context-override: #161d2e;
--diffs-bg-hover-override: #1c2438;
--diffs-bg-separator-override: #252C3D;
/* Line numbers */
--diffs-fg-number-override: #4B5768;
/* Additions — mint */
--diffs-addition-color-override: #5AC8A8;
--diffs-fg-number-addition-override: rgba(90, 200, 168, 0.5);
/* Deletions — coral */
--diffs-deletion-color-override: #E86B6B;
--diffs-fg-number-deletion-override: rgba(232, 107, 107, 0.5);
}
/* ── Server-rendered SVG graph overrides ── */
/* The server injects @media (prefers-color-scheme: dark) which won't reliably
fire when the OS is in light mode, so we apply dark graph colors always. */
.graph-svg text { fill: #c6d4e0; }
.graph-svg [stroke="#357f9e"] { stroke: #67B2D7; }
.graph-svg [stroke="#666666"] { stroke: #5a7a94; }
.graph-svg polygon[fill="#357f9e"] { fill: #67B2D7; }
.graph-svg polygon[fill="#666666"] { fill: #5a7a94; }
.graph-svg ellipse[fill="#ffffff"],
.graph-svg polygon[fill="#ffffff"] { fill: #1a2b3c; }
.graph-svg ellipse[stroke="#357f9e"],
.graph-svg polygon[stroke="#357f9e"] { stroke: #67B2D7; }
.graph-svg [fill="#1a1a1a"] { fill: #c6d4e0; }
/* ---------------------------------------------------------------------------
* assistant-ui theme overrides (--aui-* variables) — mapped to Fabro tokens.
* Values are HSL component triples so assistant-ui's hsl(var(...)) wrapper
* works. Scoped to .fabro-chat so the shadcn theme cannot leak out.
*
* Source colors come from the @theme block above:
* navy-950 #0F1729 = 220 47% 11%
* panel #252C3D = 222 24% 19%
* panel-alt #1a2133 = 223 33% 15%
* teal-500 #67B2D7 = 200 60% 62% (Fabro's "teal" is a sky blue)
* mint #5AC8A8 = 163 49% 57%
* ice-100 #E8EDF3 = 213 27% 93%
* ice-300 #A8B5C5 = 213 22% 72%
* ------------------------------------------------------------------------- */
.fabro-chat {
--aui-background: 220 47% 11%;
--aui-foreground: 0 0% 100%;
--aui-card: 222 24% 19%;
--aui-card-foreground: 0 0% 100%;
--aui-popover: 222 24% 19%;
--aui-popover-foreground: 0 0% 100%;
--aui-primary: 200 60% 62%;
--aui-primary-foreground: 220 47% 11%;
--aui-secondary: 223 33% 15%;
--aui-secondary-foreground: 0 0% 100%;
--aui-muted: 223 33% 15%;
--aui-muted-foreground: 213 22% 72%;
--aui-accent: 163 49% 57%;
--aui-accent-foreground: 220 47% 11%;
--aui-destructive: 0 76% 66%;
--aui-destructive-foreground: 0 0% 100%;
--aui-border: 218 28% 17%;
--aui-input: 218 28% 17%;
--aui-ring: 200 60% 62%;
--aui-radius: 0.5rem;
--aui-thread-max-width: 44rem;
}