Sign in
Join now
scn-chat
sharedcomputer.network
/
Star
Starred
Star
Something went wrong. Try again.
Fork
0
Eye filled
Watch
Watching
Watch
ATProto-based AI Assistant Chat interface.
Star
Starred
Star
Something went wrong. Try again.
Fork
Eye filled
Watch
Watching
Watch
Overview
Overview
Issues
Issues
1
1
Pulls
Pulls
Pipelines
Pipelines
scn-chat
/
apps
/
web
/
src
/
shared
2 folders
·
27 files
at
admin-config-export
pouriade.com
·
Settings and admin forms keep Save disabled until the form differs from what was saved, and say "Unsaved changes" beside it until then. SaveBar reads this from its form's context, and a form that adds, such as Issue key or Add plugin, passes adds so it never says so. Leaving a page doesn't ask. Preferences now starts over from the stored record after a save, as the other forms do.
·
1w ago
3c7e3ad4
schema-fields
Small settings, admin, and sidebar fixes. Renaming a chat from the sidebar won't save an empty title, which marked the chat user-titled and kept it from ever being titled automatically; EditForm takes required, and form.SubmitButton takes disabled. Secret fields use secretInput (autocomplete="new-password" and the password managers' ignore attributes), since a filled-in password would replace the saved secret, and model IDs, handles, base URLs, and slugs use literalInput, so phone keyboards don't capitalize or correct them; both live in shared/ui/controls.tsx. Removing a role member asks to confirm in place. Add user, Add member, Add model, and Create role return their mutation from onSubmit, so the button stays busy until it finishes and can't send twice, and a listed model can't be ticked twice while it is being added. Every settings and admin route has a staticData title, and pageTitle joins them deepest first, as "API keys · Settings · SCN Chat", with a page opened from a section taking that section's title.
2w ago
ui
Settings and admin forms keep Save disabled until the form differs from what was saved, and say "Unsaved changes" beside it until then. SaveBar reads this from its form's context, and a form that adds, such as Issue key or Add plugin, passes adds so it never says so. Leaving a page doesn't ask. Preferences now starts over from the stored record after a save, as the other forms do.
1w ago
api.ts
Consolidate the web app's session hooks and simplify the conversation hooks. Signing out and ending a session share signOut and toLogin in features/auth/sign-out.ts, used by useSignOut, which now returns its mutation, the viewer's sign-out, and endSession, keeping their difference: signing out shows a failed delete of the local copy, while an ended session logs it and leaves. StoreProvider reports the store's sync finding the session ended through the same listeners as a request's 401, so one useEndSessionOnUnauthorized in the signed-in layout handles both, and the chat layout keeps no Effect. A test covers it. The store's query hooks share useActiveStore, session.tsx becomes session.ts with Me an interface, replyStreamQuery moves to the conversation's queries.ts, useBranch and useReplyStream drop the useCallbacks React Compiler now covers, and useStickToBottom returns a callback ref that follows the content's scrolling parent while it is mounted, in place of an Effect bound to a ref once.
3w ago
cn.ts
Revert "A streaming reply's words come into focus as they arrive, fading in from a 4px blur as Streamdown's blurIn does, rather than only fading. The theme gains --animate-blur-in, its blur-in keyframes, and a --blur-in token for how blurred a word starts, since the app doesn't load Streamdown's stylesheet. Timing, the cascade, list markers, and reduced motion are as before."
1w ago
ErrorAlert.tsx
Add the shared UI kit in src/shared/ui. Button with default, primary, danger, and quiet tones, primary when it submits, IconButton, Badge and Chips, the field look, Switch and Radio drawing their knob and dot beside the input, the Select from design-reference on usePlacement, the floating menu look, CopyButton, item and link lists, and the settings kit of page, group, row, field, choices, SaveBar, ConfirmButton, empty state, and callout, whose rows stack in a narrow column. The form kit's fields take the field look and gain SwitchField, and ErrorAlert, RouteError, and RoutePending use the kit. The theme gains letter spacings, the radio's edge, and the copy icon's swap, and the guard lets whole extents like 100vw through.
2w ago
ErrorPage.tsx
Finish the styling port by deleting main's stand-in sheet, its legacy layer, and every data-ported marker, so every element's look is its own utilities over the theme. The shared chat page, which leaned on the old sheet, gets the conversation's column with its title and who shared it above the messages, and its sign-in and loading states center on the panel. Copy's icon swap and the Jump to button stop scaling under reduced motion. The architecture doc describes the token theme and Streamdown and lists ADRs 0004 and 0005, the web UI spec no longer calls the app plain, the styling spec marks the port done and ticks the criteria checked, and the extending spec records what the styling port already delivered.
2w ago
errors.ts
Group the web app's code by feature. Each folder in src/features (auth, chat-list, conversation, sharing, models, settings, admin) holds that feature's pages, layouts included, components, hooks, lib, and queries, and the routes stay thin, rendering a feature's page. Page files are named after the component they export. The query factories split from src/queries.ts into each feature's queries.ts, leaving staleTime in src/shared, which also holds the API client, errors, search params, and the schema forms that admin and settings share. inheritedModel moves from the models module to the conversation's branch module, so models no longer depends on the conversation. Tests mirror the new tree, and the specs, the architecture doc, and CLAUDE.md name the new paths.
3w ago
form.tsx
Settings and admin forms keep Save disabled until the form differs from what was saved, and say "Unsaved changes" beside it until then. SaveBar reads this from its form's context, and a form that adds, such as Issue key or Add plugin, passes adds so it never says so. Leaving a page doesn't ask. Preferences now starts over from the stored record after a save, as the other forms do.
1w ago
icons.tsx
A file in a sent message shows as the chip it was in the composer, with its icon, name, and kind and size, as "PDF · 1.2 MB", rather than a bare link holding its name. The chip moves to src/shared/ui as FileChip, which the composer and the message share. The whole chip is a link: a PDF opens in a new tab in the browser's own viewer, ending in an arrow out, and any other file downloads under its name, ending in a download arrow. Files side by side share a row that wraps toward the message's edge. The blob routes, the owner's and the shared one, serve a PDF inline and take application/pdf as a type, so a spaces user's PDF reaches the browser as one. The attachments, web UI, and styling specs say so.
2w ago
lines.ts
Split the web app's leaf components into smaller ones that return early. MessageView keeps the message's layout, and drawing parts moves to src/parts, as the extending spec lays out: markdown.tsx holds the renderer with its exported plugin lists and components, each part kind has a view taking { part, record, blobUrl }, and partViews in parts/index.ts maps kinds to them, matching only its own keys. Reasoning is one component, shared with the streamed reply, and SiblingPicker, Status, and StreamingReply have their own files. Source links are keyed like other parts, so two with the same URL no longer share a key. SchemaFields renders a SchemaField per property, with inputKind picking the control, and shares splitLines with RoleAdmin. SearchResults returns early from ResultList and ResultLabel in place of nested ternaries. SectionNav renders the admin and settings section links, ViewerSignOut moves from the login route to features/auth, and ModelSelect's props extend the select's directly. SchemaFields and RolePicker return a fragment rather than an array, since React Compiler silently skips a component that returns an array.
3w ago
LocalTime.tsx
Add shared primitives for the web component refactor. ErrorAlert renders an error's alert or nothing, LocalTime shows a date in the browser's locale only in the browser, and form.tsx is a createFormHook kit whose text, textarea, checkbox, and select fields bind to their field and label themselves, with a Form that submits through the form API and a SubmitButton disabled while submitting. The router's default error and pending components, RouteError and RoutePending, give every route its own boundaries, so a failed section shows its error inside its layout with a Retry that reloads it, and renderAt uses the same defaults.
3w ago
message.ts
The web app's message records are typed as the lexicon's MessageRecord, and a conversation's info as InfoRecord, from the local copy, which holds only records the server validated, through BranchMessage to the message, status, details, part, and tool views, so their reads need no casts. src/shared/message.ts holds what a status means, isPending and isFinished, which counts a status another client wrote that this app doesn't know as neither, along with a message's parts, whether it is encrypted, and who wrote it, in place of the checks written out in the message view, the message actions, the chat, the reply follower, and tool status. Tests build records with messageRecord in tests/helpers/records.ts, and the status rules are tested once.
2w ago
plural.ts
Port the chat sidebar from design-reference. The server answers GET /api/me/profile with the display name and Bluesky's CDN avatar thumbnail, read from the PDS on each request, and the browser store lists each chat's pending reply and newest complete reply and records when the index last synced. The sidebar puts New chat beside a search toggle, the tags as a folding filter, the chats under date headings with a dot for a reply in progress or one not yet seen on this device, a menu on each to rename, tag, or delete it in place, search results grouped by chat with their Markdown inline, and the account at the foot with a sync dot and a menu of where the chats live, their sync, Settings, Admin, and Sign out. New chat still creates the chat until the empty composer arrives.
2w ago
queries.ts
Group the web app's code by feature. Each folder in src/features (auth, chat-list, conversation, sharing, models, settings, admin) holds that feature's pages, layouts included, components, hooks, lib, and queries, and the routes stay thin, rendering a feature's page. Page files are named after the component they export. The query factories split from src/queries.ts into each feature's queries.ts, leaving staleTime in src/shared, which also holds the API client, errors, search params, and the schema forms that admin and settings share. inheritedModel moves from the models module to the conversation's branch module, so models no longer depends on the conversation. Tests mirror the new tree, and the specs, the architecture doc, and CLAUDE.md name the new paths.
3w ago
response.ts
Group the web app's code by feature. Each folder in src/features (auth, chat-list, conversation, sharing, models, settings, admin) holds that feature's pages, layouts included, components, hooks, lib, and queries, and the routes stay thin, rendering a feature's page. Page files are named after the component they export. The query factories split from src/queries.ts into each feature's queries.ts, leaving staleTime in src/shared, which also holds the API client, errors, search params, and the schema forms that admin and settings share. inheritedModel moves from the models module to the conversation's branch module, so models no longer depends on the conversation. Tests mirror the new tree, and the specs, the architecture doc, and CLAUDE.md name the new paths.
3w ago
route-states.tsx
Content on its way shows a skeleton in its own shape, pulsing gently on a new --color-skeleton, in place of every "Loading…" and every empty space that filled in later. Loading reads its shapes out once and fades them in after the quiet wait, while a route's pending page shows at once, since the router now holds it back 400ms and keeps it at least 300ms. The chat frame has the sidebar's logo and toggle over New chat, the chats, and the account card; the chat list keeps its rows until the local copy answers or, with no chats yet, until the index first syncs; a search shows rows until it first answers rather than saying nothing matched; the account card holds its avatar and name; a conversation holds its title and first turn, so a titled chat never flashes New chat; the model selects hold a line for the name, so a chosen model no longer flashes as unavailable; and settings and admin pages, the users list, the Sync page's switch, the Share panel, and the shared page each have theirs. The store hooks report loading as React Query's isLoading, or the store still opening in this tab, so no component checks the handover. The shared page's loader fetches the chat for a signed-in viewer and the page reads it with useSuspenseQuery, under the route's skeleton and a centered error page with Retry. A busy button keeps its label with a turning spinner before it, a little stronger than a disabled one, for Save, every submit, Load models, Rebuild, Sign in's Continue, and the rest.
2w ago
search-params.ts
Merge web-rebuild: the web app on TanStack Start. Port main's no-models composer block, role and model summary pages, API keys and Cron pages into its feature folders
3w ago
SectionNav.tsx
SectionNav's activeOptions is optional, defaulting to a section counting on the pages under it, so the settings nav no longer passes an empty object.
2w ago
SidebarLayout.tsx
Content on its way shows a skeleton in its own shape, pulsing gently on a new --color-skeleton, in place of every "Loading…" and every empty space that filled in later. Loading reads its shapes out once and fades them in after the quiet wait, while a route's pending page shows at once, since the router now holds it back 400ms and keeps it at least 300ms. The chat frame has the sidebar's logo and toggle over New chat, the chats, and the account card; the chat list keeps its rows until the local copy answers or, with no chats yet, until the index first syncs; a search shows rows until it first answers rather than saying nothing matched; the account card holds its avatar and name; a conversation holds its title and first turn, so a titled chat never flashes New chat; the model selects hold a line for the name, so a chosen model no longer flashes as unavailable; and settings and admin pages, the users list, the Sync page's switch, the Share panel, and the shared page each have theirs. The store hooks report loading as React Query's isLoading, or the store still opening in this tab, so no component checks the handover. The shared page's loader fetches the chat for a signed-in viewer and the page reads it with useSuspenseQuery, under the route's skeleton and a centered error page with Retry. A busy button keeps its label with a turning spinner before it, a little stronger than a disabled one, for Save, every submit, Load models, Rebuild, Sign in's Continue, and the rest.
2w ago
site.ts
The app has a description, set in the admin area beside its name, which the login page shows under the name in place of its fixed tagline. Every page carries it as its meta description and in Open Graph tags for link previews, with robots noindex, since each page is someone's own. A page's title is its route's staticData.title, such as Settings, Admin, or Shared chat, before the app name. The login page is the one page search engines may list: it overrides robots and names its canonical URL and og:url under PUBLIC_URL, which the server now passes with each request. /robots.txt keeps crawlers out of /api, /oauth, and /xrpc. shared/app-name.ts becomes shared/site.ts, which reads the name and description from the rendered page in the browser.
2w ago
theme.ts
Port the settings pages from design-reference. Preferences gains an Appearance card whose Theme select fixes the device on System, Light, or Dark at once, kept in localStorage rather than the preferences record, over cards for Models, Instructions, and Chats with Save in the bar at the panel's foot. API keys lists each key with its provider and masked last four characters, deleting from a trash icon that confirms in its row, over an add form with labels over its fields and Load models beside the model IDs. Plugins puts each plugin in a card of tool switches and its settings, and a plugin that fails to load shows its error with Reset. Sync says where the chats are stored over the background sync switch and Rebuild. SchemaFields renders settings as rows, with switches, units after numbers, secrets and long text under their labels, and nested objects as titled groups, settings cards are groups named by their headings, and ConfirmButton can start from an icon.
2w ago
time.ts
Port the admin area from design-reference. Users is a card to add a person over those waiting to sign in, then a search over a list of accounts, each with its initial, its DID, storage, and last activity, its roles as chips with a picker to add one, and its access at the end, where Suspend asks for a reason first. Roles and plugins are lists whose rows open their pages, which link back above their titles, a plugin's numbered in load order with its status and arrows to move it. A role's page holds its members, then its details with hosts and domains in a mono field. Access picks a registration mode from choices, with invite roles as chips that toggle. Models picks the default by a click anywhere on its row. A plugin's page holds its settings, a list of its provider's models, and a card to tick or type more. A model's capabilities are switches. Removing a role, plugin, or model, and revoking a key, confirm in place, and each sits in a red card. An issued key shows once in a green note with Copy. Cron shows the last run with a badge and the crontab line to copy. General, Turns, and Sync choose their cards per route, and the settings schemas gain clearer titles, descriptions, and units, which SchemaFields shows after a number.
2w ago
touch.ts
A Select given searchLabel opens a list of eight options or more, about as many as it shows before scrolling, with a search box named by it, and the model select gives "Search models", which finds a model by its name, its ID, or its provider through the options' new keywords. The box takes the focus as the list opens, except on a touch screen, where it waits for a tap so its keyboard doesn't cover the list. Typing narrows the options to those holding every word typed, in any case, moves to the first that matches, and says "No matches" through a status that is there from the start, so screen readers hear it. The arrow keys, Page Up, Page Down, and Enter work from the box while Home, End, and Space stay the field's, Escape closes the list and Tab moves on, each from the button, which takes the focus back as after a pick, and a letter typed on the closed button opens the list searching for it. An option's id comes from its place among every option, so the active id changes with each move and screen readers and the scroll into view follow it. The list's heading and search box sit over the options, which alone scroll, and usePlacement measures the room the reader can see, following the visual viewport as an on-screen keyboard comes and goes. touchOnly is shared with the composer from shared/touch.ts. The styling and web UI specs describe it, with tests and an acceptance criterion.
2w ago
useDebounced.ts
Group the web app's code by feature. Each folder in src/features (auth, chat-list, conversation, sharing, models, settings, admin) holds that feature's pages, layouts included, components, hooks, lib, and queries, and the routes stay thin, rendering a feature's page. Page files are named after the component they export. The query factories split from src/queries.ts into each feature's queries.ts, leaving staleTime in src/shared, which also holds the API client, errors, search params, and the schema forms that admin and settings share. inheritedModel moves from the models module to the conversation's branch module, so models no longer depends on the conversation. Tests mirror the new tree, and the specs, the architecture doc, and CLAUDE.md name the new paths.
3w ago
useDismiss.ts
Port the conversation header, message actions, and scrolling from design-reference. The header spans the panel over a fade, its title renames the chat when clicked, and a quiet toolbar holds Jump to, a blurred layer stacking your messages that scrolls to the one picked and makes it glow, Share as a floating SharePanel, and More with Rename and Sync this chat, one panel at a time. Messages carry icon actions in main's keyed list: edit, copy, and details on yours, copy, regenerate, and More with Regenerate with and details on a reply, and Stop on a pending one, and the shared view shows details. Following the conversation glides down and tracks whether the reader is at the bottom without rerendering it. Floating menus share useDismiss and MenuItem.
2w ago
useDocumentTitle.ts
An open chat's browser tab shows its title before the app name, as "Tile quotes · SCN Chat", and so does a shared chat with a title, through useDocumentTitle, since the title lives in the browser store or the shared chat's query, which the router's head never sees. Until the title is known the tab keeps the route's, an untitled chat shows "New chat", and leaving the chat puts back the title it replaced unless the router has set another since, as for Settings. The web UI spec describes it, with tests and an acceptance criterion.
2w ago
useInstantThemeSwitch.ts
Lay the styling foundation on Tailwind v4. styles/theme.css holds every token from design-reference under Tailwind's namespaces, with Tailwind's own palette, scales, and breakpoints cleared so utilities can only name the theme, and styles/base.css styles only the document. Main's stand-in look moves to styles/legacy.css, in a layer between the base and the utilities, scoped to stop at any area marked data-ported and restoring the browser defaults Preflight removes, until the port is done. A System, Light, or Dark choice is kept on the device and applied by a script in the head before the first paint, and useInstantThemeSwitch switches without smearing for the system and other tabs. The icons come over from design-reference taking a class, the logo draws inline in theme colors from the favicon's file, cn merges classes knowing the theme's names, Biome sorts classes, and a test keeps raw colors and lengths out of components.
2w ago
usePlacement.ts
A Select given searchLabel opens a list of eight options or more, about as many as it shows before scrolling, with a search box named by it, and the model select gives "Search models", which finds a model by its name, its ID, or its provider through the options' new keywords. The box takes the focus as the list opens, except on a touch screen, where it waits for a tap so its keyboard doesn't cover the list. Typing narrows the options to those holding every word typed, in any case, moves to the first that matches, and says "No matches" through a status that is there from the start, so screen readers hear it. The arrow keys, Page Up, Page Down, and Enter work from the box while Home, End, and Space stay the field's, Escape closes the list and Tab moves on, each from the button, which takes the focus back as after a pick, and a letter typed on the closed button opens the list searching for it. An option's id comes from its place among every option, so the active id changes with each move and screen readers and the scroll into view follow it. The list's heading and search box sit over the options, which alone scroll, and usePlacement measures the room the reader can see, following the visual viewport as an on-screen keyboard comes and goes. touchOnly is shared with the composer from shared/touch.ts. The styling and web UI specs describe it, with tests and an acceptance criterion.
2w ago
useSidebar.ts
A collapsed sidebar stays collapsed through a reload, never showing open first. The head's script, beside the theme's, reads the stored choice and marks the root with data-sidebar-collapsed before the first paint, which the layout's styles already follow from any ancestor, and toggling keeps that attribute current. The server's frame, which can't know the choice, needs no change on hydrating, so it no longer turns off the slide.
2w ago