From 01d5482c7d55011f33ad2726c0ec5bf8ced308f4 Mon Sep 17 00:00:00 2001 From: PerishCode Date: Wed, 10 Jun 2026 20:39:19 +0800 Subject: [PATCH 1/2] web: split atoms from business components --- AGENTS.md | 30 +- apps/client/soma/web/package.json | 3 +- apps/client/soma/web/src/App.tsx | 119 ++------ .../soma/web/src/components/ChatHeader.tsx | 27 ++ .../soma/web/src/components/ChatShell.tsx | 46 +++ .../soma/web/src/components/Composer.tsx | 33 +++ .../soma/web/src/components/SessionRail.tsx | 67 +++++ .../web/src/components/TimelineItemView.tsx | 97 ++++++ .../soma/web/src/components/Transcript.tsx | 25 ++ apps/client/soma/web/src/main.tsx | 2 +- apps/client/soma/web/src/styles.css | 279 ------------------ apps/client/soma/web/src/vite-env.d.ts | 3 + packages/components/package.json | 3 +- packages/components/src/ChatPrimitives.tsx | 54 ---- .../components/src/atoms/AppRoot/AppRoot.scss | 24 ++ .../components/src/atoms/AppRoot/AppRoot.tsx | 15 + .../components/src/atoms/Badge/Badge.scss | 25 ++ packages/components/src/atoms/Badge/Badge.tsx | 21 ++ .../components/src/atoms/Button/Button.scss | 79 +++++ .../components/src/atoms/Button/Button.tsx | 39 +++ .../src/atoms/CodeBlock/CodeBlock.scss | 13 + .../src/atoms/CodeBlock/CodeBlock.tsx | 10 + .../components/src/atoms/Heading/Heading.scss | 24 ++ .../components/src/atoms/Heading/Heading.tsx | 30 ++ .../src/atoms/IconButton/IconButton.scss | 24 ++ .../src/atoms/IconButton/IconButton.tsx | 29 ++ .../components/src/atoms/Inline/Inline.scss | 61 ++++ .../components/src/atoms/Inline/Inline.tsx | 39 +++ .../components/src/atoms/Notice/Notice.scss | 16 + .../components/src/atoms/Notice/Notice.tsx | 21 ++ packages/components/src/atoms/Pane/Pane.scss | 53 ++++ packages/components/src/atoms/Pane/Pane.tsx | 34 +++ .../src/atoms/ScrollArea/ScrollArea.scss | 20 ++ .../src/atoms/ScrollArea/ScrollArea.tsx | 28 ++ .../components/src/atoms/Stack/Stack.scss | 50 ++++ packages/components/src/atoms/Stack/Stack.tsx | 36 +++ .../components/src/atoms/Surface/Surface.scss | 76 +++++ .../components/src/atoms/Surface/Surface.tsx | 34 +++ packages/components/src/atoms/Text/Text.scss | 27 ++ packages/components/src/atoms/Text/Text.tsx | 33 +++ .../src/atoms/TextArea/TextArea.scss | 23 ++ .../src/atoms/TextArea/TextArea.tsx | 23 ++ packages/components/src/index.ts | 16 +- packages/components/src/internal/cx.ts | 3 + packages/components/src/styles.d.ts | 1 + packages/components/src/theme/default.scss | 33 +++ .../components/src/theme/tokens.border.scss | 3 + .../components/src/theme/tokens.colors.scss | 48 +++ .../components/src/theme/tokens.radius.scss | 4 + .../components/src/theme/tokens.space.scss | 8 + .../src/theme/tokens.typography.scss | 9 + pnpm-lock.yaml | 205 ++++++++++++- 52 files changed, 1589 insertions(+), 436 deletions(-) create mode 100644 apps/client/soma/web/src/components/ChatHeader.tsx create mode 100644 apps/client/soma/web/src/components/ChatShell.tsx create mode 100644 apps/client/soma/web/src/components/Composer.tsx create mode 100644 apps/client/soma/web/src/components/SessionRail.tsx create mode 100644 apps/client/soma/web/src/components/TimelineItemView.tsx create mode 100644 apps/client/soma/web/src/components/Transcript.tsx delete mode 100644 apps/client/soma/web/src/styles.css create mode 100644 apps/client/soma/web/src/vite-env.d.ts delete mode 100644 packages/components/src/ChatPrimitives.tsx create mode 100644 packages/components/src/atoms/AppRoot/AppRoot.scss create mode 100644 packages/components/src/atoms/AppRoot/AppRoot.tsx create mode 100644 packages/components/src/atoms/Badge/Badge.scss create mode 100644 packages/components/src/atoms/Badge/Badge.tsx create mode 100644 packages/components/src/atoms/Button/Button.scss create mode 100644 packages/components/src/atoms/Button/Button.tsx create mode 100644 packages/components/src/atoms/CodeBlock/CodeBlock.scss create mode 100644 packages/components/src/atoms/CodeBlock/CodeBlock.tsx create mode 100644 packages/components/src/atoms/Heading/Heading.scss create mode 100644 packages/components/src/atoms/Heading/Heading.tsx create mode 100644 packages/components/src/atoms/IconButton/IconButton.scss create mode 100644 packages/components/src/atoms/IconButton/IconButton.tsx create mode 100644 packages/components/src/atoms/Inline/Inline.scss create mode 100644 packages/components/src/atoms/Inline/Inline.tsx create mode 100644 packages/components/src/atoms/Notice/Notice.scss create mode 100644 packages/components/src/atoms/Notice/Notice.tsx create mode 100644 packages/components/src/atoms/Pane/Pane.scss create mode 100644 packages/components/src/atoms/Pane/Pane.tsx create mode 100644 packages/components/src/atoms/ScrollArea/ScrollArea.scss create mode 100644 packages/components/src/atoms/ScrollArea/ScrollArea.tsx create mode 100644 packages/components/src/atoms/Stack/Stack.scss create mode 100644 packages/components/src/atoms/Stack/Stack.tsx create mode 100644 packages/components/src/atoms/Surface/Surface.scss create mode 100644 packages/components/src/atoms/Surface/Surface.tsx create mode 100644 packages/components/src/atoms/Text/Text.scss create mode 100644 packages/components/src/atoms/Text/Text.tsx create mode 100644 packages/components/src/atoms/TextArea/TextArea.scss create mode 100644 packages/components/src/atoms/TextArea/TextArea.tsx create mode 100644 packages/components/src/internal/cx.ts create mode 100644 packages/components/src/styles.d.ts create mode 100644 packages/components/src/theme/default.scss create mode 100644 packages/components/src/theme/tokens.border.scss create mode 100644 packages/components/src/theme/tokens.colors.scss create mode 100644 packages/components/src/theme/tokens.radius.scss create mode 100644 packages/components/src/theme/tokens.space.scss create mode 100644 packages/components/src/theme/tokens.typography.scss diff --git a/AGENTS.md b/AGENTS.md index f95e6e6..dccb27b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,7 +59,9 @@ mini-stim/ │ └── web/ # Vite/React web client ├── packages/ │ ├── contracts/ # generated OpenAPI schema/types -│ └── components/ # reusable UI primitives used by client +│ ├── mqueue/ # browser transport/event projection over contracts + SSE +│ ├── hooks/ # React provider + atomic hooks over mqueue +│ └── components/ # reusable UI primitives/compositions used by client ├── docs/ └── .task/ # local task memory, ignored by git ``` @@ -99,6 +101,8 @@ plane and must stay limited to cell lifecycle facts. - Prefer one working web chat loop over architecture scaffolding. - Server product truth lives in `santi-core`; web UI consumes API contracts and must not define durable product semantics. +- `mini-stim` is self-contained, so frontend package boundaries exist for code + ownership and clarity, not for independent external release choreography. - Provider integration truth lives behind `santi-provider::ProviderClient`; `santi-core` must stay provider-agnostic. - The server soma owns HTTP routing, SSE framing, and OpenAPI export through the @@ -112,6 +116,30 @@ plane and must stay limited to cell lifecycle facts. fallbacks or exploring unrelated environment workarounds unless the user asks. - Keep hard cuts acceptable. Add compatibility only for a real external surface. +## Frontend Package Boundary + +- Keep the frontend split strict even though everything ships from one repo. +- `packages/contracts` owns generated OpenAPI clients and DTOs only. +- `packages/mqueue` owns browser-facing HTTP calls, SSE wiring, stream merge, + event projection, and any direct use of `@mini-stim/contracts`. +- `packages/hooks` owns React context/providers and atomic hooks over + `mqueue`. It is the only stateful integration layer the web app should + consume. +- `packages/components` owns reusable presentational UI primitives and small + compositions. It should stay transport-agnostic and product-light. +- `apps/client/soma/web` owns route/page assembly, product-specific layout, + and composer/transcript/session UX built from hooks and components. +- Web app code must not call raw `fetch`, construct `EventSource`, import + `@mini-stim/contracts`, or reach into sidecar/browser globals directly. +- If a UI pattern is reusable across multiple client surfaces or would + otherwise cause page-level CSS/control duplication, move it into + `packages/components` instead of re-implementing it in `web`. +- If logic is about transport, replay, stream state, or event normalization, it + belongs below `web`, usually in `mqueue` or `hooks`, not inside React pages. +- Do not create a fake package/release process inside the repo. Keep the + boundary architectural and local-first: workspace packages, direct + consumption, and simple builds are enough. + ## Data Modeling Rules Keep the simplified model normalized: diff --git a/apps/client/soma/web/package.json b/apps/client/soma/web/package.json index 8cb51ab..4aa01d4 100644 --- a/apps/client/soma/web/package.json +++ b/apps/client/soma/web/package.json @@ -18,6 +18,7 @@ }, "devDependencies": { "@types/react": "^19.2.16", - "@types/react-dom": "^19.2.3" + "@types/react-dom": "^19.2.3", + "sass": "^1.94.0" } } diff --git a/apps/client/soma/web/src/App.tsx b/apps/client/soma/web/src/App.tsx index 2c8fa84..1597f8b 100644 --- a/apps/client/soma/web/src/App.tsx +++ b/apps/client/soma/web/src/App.tsx @@ -1,5 +1,5 @@ import { useMemo, useState } from "react"; -import { Composer } from "@mini-stim/components"; +import { AppRoot } from "@mini-stim/components"; import { useMessageConnection, useSelectedSessionId, @@ -8,9 +8,11 @@ import { useSessionPending, useSessionTimeline, useSessions, - type TimelineItem, } from "@mini-stim/hooks"; +import { ChatShell } from "./components/ChatShell"; +import { SessionRail } from "./components/SessionRail"; + export function App() { const sessions = useSessions(); const selectedSessionId = useSelectedSessionId(); @@ -66,98 +68,33 @@ export function App() { } return ( -
- -
-
-

{selectedTitle}

- {busy ? Sending : null} - {selectedSessionId ? {connection} : null} -
-
- {timeline.map((item) => renderTimelineItem(item))} - {!timeline.length ?
Start a session
: null} -
- {visibleError ?
{visibleError}
: null} - -
-
+ + } + main={ + + } + /> ); } function sessionLabel(session: { id: string; title?: string | null }) { return session.title?.trim() || session.id; } - -function renderTimelineItem(item: TimelineItem) { - if (item.kind === "message") { - const role = item.message.message.actor_type; - return ( -
-
{item.message.content_text}
-
- ); - } - - if (item.kind === "tool_call") { - const result = item.toolResult; - const failed = Boolean(result?.error_text); - return ( -
-
- {item.toolCall.tool_name} - {result ? (failed ? "failed" : "completed") : "running"} -
-
{formatJson(item.toolCall.arguments)}
- {result ? ( -
-            {result.error_text ?? formatJson(result.output)}
-          
- ) : null} -
- ); - } - - const failed = Boolean(item.toolResult.error_text); - return ( -
-
- tool result - {failed ? "failed" : "completed"} -
-
-        {item.toolResult.error_text ?? formatJson(item.toolResult.output)}
-      
-
- ); -} - -function formatJson(value: unknown) { - if (value === null || value === undefined) { - return ""; - } - if (typeof value === "string") { - return value; - } - return JSON.stringify(value, null, 2); -} diff --git a/apps/client/soma/web/src/components/ChatHeader.tsx b/apps/client/soma/web/src/components/ChatHeader.tsx new file mode 100644 index 0000000..485eb59 --- /dev/null +++ b/apps/client/soma/web/src/components/ChatHeader.tsx @@ -0,0 +1,27 @@ +import { + Badge, + Heading, + Inline, + Pane, +} from "@mini-stim/components"; + +export function ChatHeader(props: { + busy: boolean; + connection: string; + selectedSessionId: string | null; + title: string; +}) { + return ( + + + + {props.title} + + + {props.busy ? Sending : null} + {props.selectedSessionId ? {props.connection} : null} + + + + ); +} diff --git a/apps/client/soma/web/src/components/ChatShell.tsx b/apps/client/soma/web/src/components/ChatShell.tsx new file mode 100644 index 0000000..0d3d18c --- /dev/null +++ b/apps/client/soma/web/src/components/ChatShell.tsx @@ -0,0 +1,46 @@ +import { Notice, Pane, Stack, Text } from "@mini-stim/components"; + +import { ChatHeader } from "./ChatHeader"; +import { Composer } from "./Composer"; +import { Transcript } from "./Transcript"; + +export function ChatShell(props: { + busy: boolean; + connection: string; + error: string | null; + onDraftChange: (value: string) => void; + onSend: () => void; + selectedSessionId: string | null; + title: string; + timeline: Parameters[0]["timeline"]; + draft: string; +}) { + return ( + + + + + {props.error ? ( + + + {props.error} + + + ) : null} + + + + + + ); +} diff --git a/apps/client/soma/web/src/components/Composer.tsx b/apps/client/soma/web/src/components/Composer.tsx new file mode 100644 index 0000000..9fd930e --- /dev/null +++ b/apps/client/soma/web/src/components/Composer.tsx @@ -0,0 +1,33 @@ +import { IconButton, Inline, TextArea } from "@mini-stim/components"; + +export function Composer(props: { + disabled?: boolean; + onChange: (value: string) => void; + onSubmit: () => void; + value: string; +}) { + return ( +
{ + event.preventDefault(); + props.onSubmit(); + }} + > + +