From dbada4b0f198cd43ced87c5265a3b9fb92a6acd0 Mon Sep 17 00:00:00 2001 From: PerishCode Date: Fri, 12 Jun 2026 11:09:35 +0800 Subject: [PATCH 01/19] core: split session and soul profiles --- apps/client/soma/web/src/App.tsx | 11 +- .../soma/web/src/components/SessionRail.tsx | 19 +-- apps/server/soma/crates/santi-api/src/main.rs | 14 +- .../soma/crates/santi-core/src/model.rs | 35 ++++- .../soma/crates/santi-core/src/service.rs | 32 +++-- .../soma/crates/santi-core/src/store.rs | 77 ++++++++--- .../soma/crates/santi-core/src/store/db.rs | 74 +++++++++-- .../soma/crates/santi-core/src/store/rows.rs | 62 ++++++++- .../crates/santi-core/src/store/schema.rs | 20 ++- .../soma/crates/santi-core/tests/service.rs | 8 +- .../soma/crates/santi-core/tests/store.rs | 29 +++-- packages/contracts/openapi.json | 121 ++++++++++++++++-- packages/contracts/src/openapi.ts | 41 +++++- packages/hooks/src/index.tsx | 5 +- packages/mqueue/src/index.ts | 56 ++++---- packages/mqueue/src/types.ts | 6 +- 16 files changed, 478 insertions(+), 132 deletions(-) diff --git a/apps/client/soma/web/src/App.tsx b/apps/client/soma/web/src/App.tsx index 999b974..5f2529c 100644 --- a/apps/client/soma/web/src/App.tsx +++ b/apps/client/soma/web/src/App.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import { AppRoot, Grid, GridItem } from "@mini-stim/components"; import { + type SessionSummary, useDebouncedValue, useMessageConnection, useSelectedSessionId, @@ -84,11 +85,11 @@ export function App() { const visibleError = error ?? sessionErrorMessage; const debouncedConnection = useDebouncedValue(connection, { debounceMs: 150 }); const selectedTitle = useMemo(() => { - const selected = sessions.find((session) => session.id === selectedSessionId); + const selected = sessions.find((session) => session.session.id === selectedSessionId); return selected ? sessionLabel(selected) : "New session"; }, [selectedSessionId, sessions]); const selectedSession = useMemo( - () => sessions.find((session) => session.id === selectedSessionId) ?? null, + () => sessions.find((session) => session.session.id === selectedSessionId) ?? null, [selectedSessionId, sessions], ); @@ -167,7 +168,7 @@ export function App() { runtime={runtime} selectedSessionId={selectedSessionId} title={selectedTitle} - titleValue={selectedSession?.title ?? null} + titleValue={selectedSession?.profile.title ?? null} timeline={timeline} draft={draft} /> @@ -177,6 +178,6 @@ export function App() { ); } -function sessionLabel(session: { id: string; title?: string | null }) { - return session.title?.trim() || session.id; +function sessionLabel(session: SessionSummary) { + return session.profile.title?.trim() || session.session.id; } diff --git a/apps/client/soma/web/src/components/SessionRail.tsx b/apps/client/soma/web/src/components/SessionRail.tsx index a7cd0d6..108e3e2 100644 --- a/apps/client/soma/web/src/components/SessionRail.tsx +++ b/apps/client/soma/web/src/components/SessionRail.tsx @@ -11,7 +11,7 @@ import { Text, Timestamp, } from "@mini-stim/components"; -import type { Session } from "@mini-stim/hooks"; +import type { SessionSummary } from "@mini-stim/hooks"; export function SessionRail(props: { busy: boolean; @@ -19,7 +19,7 @@ export function SessionRail(props: { onSelect: (sessionId: string) => void; previews: Record; selectedSessionId: string | null; - sessions: Session[]; + sessions: SessionSummary[]; }) { return ( @@ -48,24 +48,25 @@ export function SessionRail(props: { {props.sessions.map((session) => { - const selected = session.id === props.selectedSessionId; - const label = sessionLabel(session, props.previews[session.id]); + const id = session.session.id; + const selected = id === props.selectedSessionId; + const label = sessionLabel(session, props.previews[id]); return ( diff --git a/apps/client/soma/web/src/components/ChatShell.tsx b/apps/client/soma/web/src/components/ChatShell.tsx index 61f40fe..65d0eda 100644 --- a/apps/client/soma/web/src/components/ChatShell.tsx +++ b/apps/client/soma/web/src/components/ChatShell.tsx @@ -1,9 +1,7 @@ import { Pane, SectionStackLayout } from "@mini-stim/components"; -import type { SessionRuntimeSnapshot } from "@mini-stim/hooks"; import { ChatHeader } from "./ChatHeader"; import { Composer } from "./Composer"; -import { InspectPanel } from "./InspectPanel"; import { Transcript } from "./Transcript"; export function ChatShell(props: { @@ -16,7 +14,6 @@ export function ChatShell(props: { onSend: () => void; onTitleCommit: (title: string | null) => void; onToggleInspect: () => void; - runtime: SessionRuntimeSnapshot | null; selectedSessionId: string | null; title: string; titleValue: string | null; @@ -39,13 +36,7 @@ export function ChatShell(props: { titleValue={props.titleValue} /> )} - middle={ - props.inspecting ? ( - - ) : ( - - ) - } + middle={} bottom={( & { gap?: "none" | "shell" | "sm" | "md"; grow?: boolean; - template?: "sidebar-main"; + template?: "sidebar-main" | "sidebar-main-inspect"; }; type GridItemProps = ComponentPropsWithoutRef<"div"> & { - area: "sidebar" | "main"; + area: "inspect" | "main" | "sidebar"; tag?: "aside" | "div" | "main" | "section"; }; From 023c1e181d3aa16ebb288c56184afaa2ce7bc530 Mon Sep 17 00:00:00 2001 From: PerishCode Date: Fri, 12 Jun 2026 12:27:06 +0800 Subject: [PATCH 05/19] web: persist resizable pane widths --- apps/client/soma/web/src/App.tsx | 96 +++++++++++++++---- packages/components/src/atoms/Grid/Grid.scss | 1 + packages/components/src/atoms/Grid/Grid.tsx | 13 ++- .../src/atoms/ResizeHandle/ResizeHandle.scss | 45 +++++++++ .../src/atoms/ResizeHandle/ResizeHandle.tsx | 27 ++++++ packages/components/src/index.ts | 1 + 6 files changed, 163 insertions(+), 20 deletions(-) create mode 100644 packages/components/src/atoms/ResizeHandle/ResizeHandle.scss create mode 100644 packages/components/src/atoms/ResizeHandle/ResizeHandle.tsx diff --git a/apps/client/soma/web/src/App.tsx b/apps/client/soma/web/src/App.tsx index 89d7f79..c7979a0 100644 --- a/apps/client/soma/web/src/App.tsx +++ b/apps/client/soma/web/src/App.tsx @@ -1,5 +1,5 @@ -import { useEffect, useMemo, useState, type CSSProperties } from "react"; -import { AppRoot, Grid, GridItem, Pane } from "@mini-stim/components"; +import { useEffect, useMemo, useState, type PointerEvent as ReactPointerEvent } from "react"; +import { AppRoot, Grid, GridItem, Pane, ResizeHandle } from "@mini-stim/components"; import { uiStorage } from "@mini-stim/storage"; import { type SessionSummary, @@ -19,8 +19,15 @@ import { ChatShell } from "./components/ChatShell"; import { InspectPanel } from "./components/InspectPanel"; import { SessionRail } from "./components/SessionRail"; +const DEFAULT_RAIL_WIDTH = 304; +const DEFAULT_INSPECT_WIDTH = 352; +const RAIL_WIDTH_RANGE = { min: 264, max: 420 }; +const INSPECT_WIDTH_RANGE = { min: 288, max: 520 }; + export function App() { const [preferences] = useState(() => uiStorage.read()); + const [layout, setLayout] = useState(preferences.desktopLayout); + const [resizing, setResizing] = useState<"inspect" | "rail" | null>(null); const sessions = useSessions(); const selectedSessionId = useSelectedSessionId(); const timeline = useSessionTurnTimeline(); @@ -105,10 +112,6 @@ export function App() { () => sessions.find((session) => session.session.id === selectedSessionId) ?? null, [selectedSessionId, sessions], ); - const gridStyle = useMemo( - () => layoutStyle(preferences.desktopLayout), - [preferences.desktopLayout], - ); function createNewSession() { setError(null); @@ -158,13 +161,65 @@ export function App() { } } + function beginRailResize(event: ReactPointerEvent) { + beginResize("rail", event); + } + + function beginInspectResize(event: ReactPointerEvent) { + beginResize("inspect", event); + } + + function beginResize( + target: "inspect" | "rail", + event: ReactPointerEvent, + ) { + event.preventDefault(); + const startX = event.clientX; + const startWidth = + event.currentTarget.parentElement?.getBoundingClientRect().width ?? + (target === "rail" ? DEFAULT_RAIL_WIDTH : DEFAULT_INSPECT_WIDTH); + let nextWidth = startWidth; + setResizing(target); + + function handlePointerMove(moveEvent: PointerEvent) { + const delta = moveEvent.clientX - startX; + nextWidth = clampWidth( + target === "rail" ? startWidth + delta : startWidth - delta, + target, + ); + setLayout((current) => ({ + ...current, + railWidthPx: target === "rail" ? nextWidth : current.railWidthPx, + inspectWidthPx: target === "inspect" ? nextWidth : current.inspectWidthPx, + })); + } + + function handlePointerUp() { + window.removeEventListener("pointermove", handlePointerMove); + window.removeEventListener("pointerup", handlePointerUp); + setResizing(null); + uiStorage.update((current) => ({ + ...current, + desktopLayout: { + ...current.desktopLayout, + railWidthPx: target === "rail" ? nextWidth : layout.railWidthPx, + inspectWidthPx: target === "inspect" ? nextWidth : layout.inspectWidthPx, + }, + })); + } + + window.addEventListener("pointermove", handlePointerMove); + window.addEventListener("pointerup", handlePointerUp); + } + return ( + + ) : null} @@ -210,16 +277,7 @@ function sessionLabel(session: SessionSummary) { return session.profile.title?.trim() || session.session.id; } -function layoutStyle(layout: { - railWidthPx: number | null; - inspectWidthPx: number | null; -}): CSSProperties { - const style: Record = {}; - if (layout.railWidthPx) { - style["--ms-grid-sidebar-width"] = `${layout.railWidthPx}px`; - } - if (layout.inspectWidthPx) { - style["--ms-grid-inspect-width"] = `${layout.inspectWidthPx}px`; - } - return style as CSSProperties; +function clampWidth(width: number, target: "inspect" | "rail") { + const range = target === "rail" ? RAIL_WIDTH_RANGE : INSPECT_WIDTH_RANGE; + return Math.round(Math.min(range.max, Math.max(range.min, width))); } diff --git a/packages/components/src/atoms/Grid/Grid.scss b/packages/components/src/atoms/Grid/Grid.scss index 6bfb127..0d2da04 100644 --- a/packages/components/src/atoms/Grid/Grid.scss +++ b/packages/components/src/atoms/Grid/Grid.scss @@ -48,6 +48,7 @@ $grid-gaps: ( } &__item { + position: relative; display: flex; min-width: 0; min-height: 0; diff --git a/packages/components/src/atoms/Grid/Grid.tsx b/packages/components/src/atoms/Grid/Grid.tsx index ff10899..4d119cf 100644 --- a/packages/components/src/atoms/Grid/Grid.tsx +++ b/packages/components/src/atoms/Grid/Grid.tsx @@ -1,4 +1,4 @@ -import type { ComponentPropsWithoutRef } from "react"; +import type { ComponentPropsWithoutRef, CSSProperties } from "react"; import { cx } from "../../internal/cx"; import "./Grid.scss"; @@ -6,6 +6,8 @@ import "./Grid.scss"; type GridProps = ComponentPropsWithoutRef<"div"> & { gap?: "none" | "shell" | "sm" | "md"; grow?: boolean; + inspectWidthPx?: number | null; + sidebarWidthPx?: number | null; template?: "sidebar-main" | "sidebar-main-inspect"; }; @@ -18,12 +20,21 @@ export function Grid({ className, gap = "none", grow = false, + inspectWidthPx = null, + sidebarWidthPx = null, + style, template = "sidebar-main", ...props }: GridProps) { + const gridStyle = { + ...style, + ...(sidebarWidthPx ? { "--ms-grid-sidebar-width": `${sidebarWidthPx}px` } : null), + ...(inspectWidthPx ? { "--ms-grid-inspect-width": `${inspectWidthPx}px` } : null), + } as CSSProperties; return (
& { + placement: "end" | "start"; +}; + +export function ResizeHandle({ + className, + placement, + type = "button", + ...props +}: ResizeHandleProps) { + return ( +