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
Commits on
rate-limits
hadsie.com
dead7601
Copied
Failed
Copy SHA
Retry busy providers. Don't retry spent keys. Add messaging for users.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
20e80e3d
Copied
Failed
Copy SHA
A reply's sources sit behind their count, which opens like a dropdown onto a numbered list with one source to a line, rather than badges wrapped side by side under a count. The count is a details summary with a chevron, and it starts closed, so the sources stay out of the reply's way until asked for. Each row is as wide as its link, in the well's grey, so its hover tint covers the link rather than the line. The list no longer folds past seven, since opening the count already asks for every source; a web search's results still fold. The web UI spec says so, and the message test checks the sources start closed and open to show all ten.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
53fb89d2
Copied
Failed
Copy SHA
A footnote link scrolls to its note in the same reply rather than opening the page in a new tab, and the note's way back does the same. Every reply numbers its footnotes from one, so their ids repeat down the page and a plain jump would land in the first reply's notes; the link looks for its target inside its own reply instead. Streamdown's hardening gives every link a new tab, so a link within the text drops it. Other links still open in a new tab. The web UI spec says so, and the Markdown test renders two replies with the same footnote and checks the jump stays in the one clicked.
v0.1.1
Copy SHA
Browse repository
committed
2w ago
pouriade.com
0ba302fe
Copied
Failed
Copy SHA
Selected text is a calm blue, its own selection token, rather than the pale amber it shared with search matches and the Stopped badge. Blue is the one hue nothing else in the app means anything by, so a selection never reads as a match, a notice, or the green of an action. Text keeps its own ink on it: body text clears 7:1 and muted text 4.5:1 in both modes.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
e7b80fba
Copied
Failed
Copy SHA
A stopped reply says so with a small amber Stopped badge, carrying the Stop button's square, under where it broke off, rather than a loose line of muted text that read like the end of the reply. The badge is the shared Badge with a new warning tone, from the highlight tokens, so it stands apart from the grey source chips. The web UI spec says so, and the message test looks for the badge.
v0.1.0
Copy SHA
Browse repository
committed
2w ago
pouriade.com
a9135322
Copied
Failed
Copy SHA
The chat list and its rows do less work, from an audit against React performance practice. Opening a search hides the chat list inside React's Activity rather than unmounting it, so closing the search brings the chats back as they were rather than mounting every row again. A chat row no longer holds three mutations: renaming and tagging are an EditChat card and deleting a DeleteChat card, each holding its own, so an idle row holds none, and cancelling a failed change clears its error with the card. Picking a tag keeps the rows shown until its chats arrive, rather than a skeleton between. The month headings of older chats come from two formatters made once, rather than one made for each chat on every change to the list. A reply's sources and a search's results take the fold's ref apart, so React Compiler compiles them rather than silently leaving them out. The conversation's rename, instructions, and sync call their mutations' own functions, so the header no longer renders with every state either passes through, and its jump outline is built only while the jump stack is open. The search spec says the list stays mounted, and the list test checks it is hidden.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
cc3e6a1d
Copied
Failed
Copy SHA
Fixes from the review of the performance work. A reply's stream batch is sent on the next frame or after 100ms, whichever comes first, so a reply streamed in a hidden tab, where frames don't run, still reaches its views and their step clocks as it goes, and a cancelled stream closes its connection and sends nothing more, where a late event could write to the cancelled stream and throw. A code block is colored from its first frame only when its finished code was highlighted before, as when a reply's record replaces its stream, since Shiki answers through a promise even once its grammar has loaded: highlight.ts keeps the tokens of the last 200 finished blocks for that, and the first render reads them without starting a second highlight, which it did for every new block, and the web UI spec now says so rather than claiming every block of a loaded language. The step clock keeps the times of the latest 500 steps, forgetting the oldest first. Starting or ending a regeneration rerenders only the Try again buttons, which read it from a Regenerating context, rather than every message. Both blob routes set their headers through one setBlobHeaders, which adds Vary: Cookie, so another account signed in on the same browser asks the server again rather than reading a blob from the cache. A row's sibling fields come from Siblings, the hook behind a step's body is named body, the chat list asks router.matchRoute whether a deleted chat is open rather than casting the router's params, sign-out passes the mutation's own function, and ConversationView takes pick and send out of chat with the others. The web UI spec says images load as they near the screen, and the attachments spec names Vary: Cookie. Tests cover the hidden tab, the cancelled stream, and the step clock's limit.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
31752f8c
Copied
Failed
Copy SHA
The sidebar's chat rows and account no longer render with every change to the list. The account menu was given its sync dot and status as elements built on the sync mutation, and sign-out as a function calling the sign-out mutation, and a mutation is a new object on every render, so each change to the chats rendered the account menu, card, and avatar again, seven times on a send. The account is now its own component holding its sync, given only a sign-out function that stays the same. Deleting a chat checked whether it was the open one through a callback React Compiler kept on the router's state, which changes on every navigation, so opening a chat rendered every row again. It now asks the router through a helper outside the component, so the callback stays the same. Checked in the running app: opening another chat renders no row and no account, and a send renders only the open chat's row and the sync dot.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
c10c6fcd
Copied
Failed
Copy SHA
Older messages no longer render again on send. The Try again callback every message's row gets called chat.regenerate as a method, so React Compiler kept it on the whole chat object, which changes with every update, and every row ran again three times per send, though each then reused its message. ConversationView now takes regenerate, regenerating, and stop out of chat first, so the callbacks stay the same. Checked in the running app over a send and its reply in an 18-message chat: only the new message and the reply rendered.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
88da0e13
Copied
Failed
Copy SHA
A finished step's start time is forgotten once its duration is recorded, since the duration is all it needs from then on, so the step clock no longer keeps every start seen in the session. A reply's waiting header counts its wait with a timer that wakes just after each whole second since the reply was asked for, rather than checking four times a second, and a test checks it counts a second at a time with one timer.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
60fa9dff
Copied
Failed
Copy SHA
Shiki's Oniguruma engine, a 600 KB wasm, is no longer built. Shiki's full bundle names it for its shorthand highlighter, which the app never uses, since @streamdown/code highlights with Shiki's JavaScript engine, so the wasm was built and precompressed on every build but never loaded. vite.config.ts aliases shiki/wasm to a stand-in, parts/no-wasm.ts, that fails to load with a message saying why, so anything that did reach for the engine would say so rather than highlight nothing. Code built through the alias still highlights in Chrome. The web UI spec says so beside the rehype-raw stand-in.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
86edb867
Copied
Failed
Copy SHA
Once Shiki and a language's grammar have loaded, a code block in that language is colored from its first frame, rather than drawn plain and laid out again in color a moment later, as every block was when a reply's record replaced its stream or a chat opened. The loaded highlighter is kept, so a block highlights while it first renders and in its layout effect, before the browser paints, and only the first block of a session waits for the import. The web UI spec says so, and a test checks a block is colored on its first render once its language has loaded.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
171fb03a
Copied
Failed
Copy SHA
A change to the open conversation, such as a message arriving or a reply finishing, renders again only the messages it changed, rather than every message and its actions. BranchView gives each message's row only its record, which the local copy keeps the same object while unchanged, its place among its versions as numbers and the rkeys on either side, and functions that stay the same from render to render: useChat's stop and regenerate call the mutations' own stable functions, and useModels gives the same empty list until models arrive. The reply running at the end of the branch, whose Stop the composer offers instead, reaches the Stop actions through a RunningReply context rather than the operations every action gets, so a reply starting or finishing rerenders only those. The web UI spec says so, and a ConversationView test counts each message's renders as a reply finishes.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
b78e2fb0
Copied
Failed
Copy SHA
A reply's stream is reduced in batches of the events that arrive within one animation frame, so a reply streaming a word at a time renders, and Streamdown completes and splits its whole text, at most once a frame rather than once a word. The status event is passed on at once with whatever came before it, and a dropped or unreadable stream still keeps every event that arrived first. A thought or tool step renders its body only once it is first opened, and keeps it once closed, so a closed thought no longer parses every token streaming into it and a closed tool step no longer holds all the tool returned. The body still unfolds, since it arrives while the step grows from nothing. The web UI spec and the architecture overview say so, the stream tests cover the batches, and the tool view tests open each step before reading it.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
5d56011a
Copied
Failed
Copy SHA
Pictures in a chat load once they near the screen and decode off the main thread, so opening a long chat no longer fetches and decodes every picture in it. Blobs are served with Cache-Control: private, max-age=31536000, immutable, on the owner's route and the shared one, since their URL names the CID and their bytes never change, so the browser keeps them rather than downloading each picture again on every visit. The attachments and sharing specs say so, and the route tests check the header.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
e2411fa6
Copied
Failed
Copy SHA
Sign in's Continue reads "Signing in…" once it is clicked, and its arrow gives way to the button's spinner, since it is leaving for the PDS rather than waiting on something it was asked to do. Coming back with the browser's back button shows Continue and its arrow again. The web UI spec makes it the one busy button that changes its label, and the test checks the new label and that the spinner is its only icon.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
8350b775
Copied
Failed
Copy SHA
The UI and code fonts are each a native stack that covers every platform, so the app loads no web fonts. The sans drops ui-sans-serif, which only Safari reads, and "SF Pro Text", which system-ui already reaches, names Helvetica and Arial for Linux, where they map to lookalikes, and ends with the emoji fonts of each platform after sans-serif, Noto Color Emoji among them for Linux and Android. The mono gives SF Mono in Safari and on iOS through ui-monospace, Menlo in Chrome and Firefox on a Mac, which can't reach SF Mono, Cascadia Mono or Consolas on Windows, and Liberation Mono or DejaVu Sans Mono on Linux, and no longer names JetBrains Mono, which is only there when someone installed it.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
aff65526
Copied
Failed
Copy SHA
The docs catch up with the last week's changes. The architecture overview says a local server moves to the next free port when its own is taken, and a deployment's never does, that features reach the browser's local copy only through the store's hooks, with the open conversation as useChat and the reply follower showing a reply's stream until its record arrives, that the send route calls the runner's send with the request convention in turns/request.ts, that the pending reply is claimed at <key>.r<attempt> with a create-only call and streams over server-sent events with nothing more written meanwhile, and that each user's ChatService sits over their record and blob stores, with createChatServices the one place that picks the backend. The deployment guide sets NODE_ENV to production, leaves OAUTH_SCOPE_MODE unset since it defaults to permission-set, and mentions pnpm preview. The plugin guide says a secret that can't be decrypted reads as unset, and that options can be secret too. .env.example shows OAUTH_SCOPE_MODE commented out under its comment, which had lost its variable.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
34fd2ed0
Copied
Failed
Copy SHA
When the app runs on this machine and its port is taken, the server moves to the next free one, trying up to twenty, and PUBLIC_URL with it, since the OAuth redirect and the app's links name that address, and logs a warning saying so. It moves only when PUBLIC_URL is loopback on that same port, so a deployment, or a loopback URL that names another port, keeps its port and fails to start as before, since a reverse proxy points at it. A port counts as taken when it is at every address or at 127.0.0.1 or ::1 alone, since macOS lets a listener on every address share a port with one on 127.0.0.1, which then still gets the requests for http://127.0.0.1:3000. The tunnel picks the free port itself and points both ssh and the server at it. The foundation spec and .env.example describe it, and the tests cover the probe and when the environment moves.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
ba702282
Copied
Failed
Copy SHA
A web search's results and a reply's sources show their first seven, and the rest fold behind a button that shows them in place and becomes "Show less": "Show 3 more" under the results, and a "+3 more" badge at the end of the sources, whose count still says how many there are in all. A list that would hide only one shows it instead, opening one moves focus to the first link it showed, and both fold through useFold in parts/fold.ts. A thought now reads as a tool step does: a row with its chevron, "Thought for 12s" or "Thinking", and a muted glimpse of its first line with its Markdown left out, which opens onto the whole thought, in place of three lines fading out under a Show more. The tool views and web UI specs describe both, and the tests cover the folds and the thought's row.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
83a45b30
Copied
Failed
Copy SHA
Update .env.example
Copy SHA
Browse repository
committed
2w ago
pouriade.com
02d2d188
Copied
Failed
Copy SHA
The overrides that pin the atproto alpha packages, and the dependencies allowed to run build scripts, live in pnpm-workspace.yaml rather than the "pnpm" field of package.json, which pnpm 11 no longer reads and warns about on every command. onlyBuiltDependencies becomes allowBuilds, which pnpm 10.33 also reads and turns back into the same allowlist, so the pinned pnpm and a global pnpm 11 install the same tree, and the frozen lockfile is unchanged. CLAUDE.md and the foundation spec say where the overrides now live.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
9a802e02
Copied
Failed
Copy SHA
A reply's tool calls show as what they did. A web search reads "Searched the web" with its query and opens onto its results, each its title linking to the page in a new tab, its site, and its snippet as plain text, or "No results." A page fetch reads "Read a page" with its address and opens onto the page's title and site, which of its characters the call read when there were more, and the text it read as plain text. The live line says "Searching the web…" and "Reading a page…", and a finished reply's steps are summed up as "Thought for 12s · Searched the web ×2 · Read a page". Each tool's entry in toolViews, in parts/tools/, gives its label, its activity, the glimpse of its input on its row, and its view, and a tool without one reads by its name with its input and output as before, which a view also falls back to for an error, output kept in a blob, or output it can't read. Each call has one status from toolStatus, running, done, error, or incomplete, which its badge, number, timer, and the live line all read, so calls made at once all show as running until their results arrive, where before only the last did, and a call cut off without its result says incomplete. Tool output stays text: no HTML, Markdown, images, or favicons, and links only to web pages. The tool views spec, split out of the extending spec, describes it, the plugin guide tells tool authors where a view goes, and the tests render replies through MessageView.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
fc2c1b88
Copied
Failed
Copy SHA
A search result's snippet of a reply renders its Markdown through Streamdown, as the reply itself does, so the web app no longer depends on react-markdown or remark-gfm. It uses Streamdown's Block rather than the Streamdown component, which would wrap the snippet's run of text in a div and complete the Markdown the snippet's cut left open, so the snippet renders the same elements as before and the tests pass unchanged. The plugin that marks matches is a named function, since Streamdown caches its parsers by the names of their plugins.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
867d1095
Copied
Failed
Copy SHA
A reply streams at an even pace, a word at a time, or a character at a time for Chinese and Japanese, where before it arrived in whatever bursts the provider sent. The turn's streamText uses the AI SDK's smoothStream with a chunking pattern of our own, since the SDK's word chunking waits for a space and would hold back Chinese and Japanese until a line ended. It waits 10 ms after each piece of text and reasoning alike, which caps a reply at about 100 words a second, so a faster model's record is written once smoothing catches up, within the turn's timeout. OpenAI-compatible models now show reasoning a server leaves in the reply inside <think> tags, such as llama.cpp with --reasoning-format none, as reasoning rather than as part of the answer, through the AI SDK's extractReasoningMiddleware, so that plugin now depends on the ai package. Its new startWithReasoning option is for models such as DeepSeek R1 whose template opens the tag in the prompt, so a reply has only the closing tag. The titles plugin refuses output that stopped at its output limit, such as reasoning that never reached its closing tag. The chat turns, providers, and titles specs describe it, along with the reasoning middleware dropping a reply's last characters when they could start a tag, which is a bug in the AI SDK, and docs/plugins.md says when a provider needs the ai package. Tests cover the smoothing, reasoning in think tags with and without the opening tag, whether streamed or not, and the cut-off title.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
d210fc70
Copied
Failed
Copy SHA
A conversation on its way shows a few paragraphs of text lines, spaced as a reply's are, in place of one turn's bubble, name, and paragraph, which read as a chat with one message rather than messages loading. MessagesSkeleton replaces TurnSkeleton, and the shared page shows it under its title too. The page's frame, which the server sends, now holds the conversation header's row with a line for its title, so the lines start where they will once the conversation loads, rather than under the sidebar's toggle on a narrow screen. The header's row and its title on its way live in HeaderFrame.tsx, which the header and the frame share, so the frame stays free of the header's panels and menus. The web UI spec describes it.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
63773430
Copied
Failed
Copy SHA
Update README.md
Copy SHA
Browse repository
committed
2w ago
pouriade.com
c8dfa1a3
Copied
Failed
Copy SHA
pnpm dev:remote and pnpm preview:remote run the dev server or the production build behind a localhost.run tunnel, for testing from a phone or another device. tunnel-cli.ts opens the tunnel over ssh, which needs no account and nothing to install, and starts the server with the tunnel's HTTPS URL as PUBLIC_URL for that run alone, leaving .env as it is, and Vite allowing the tunnel's host. Closing it closes the tunnel, and the tunnel closing stops the server. localhost.run because it streams a reply as it is written and shows no warning page in front of sign-in, where a Cloudflare quick tunnel holds back the stream until the reply ends, and Pinggy and serveo put a warning page in front of the app. Its free tier is slow, so a first load takes a while. The README, CLAUDE.md, and the foundation spec's scripts describe it.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
2ee2fcb6
Copied
Failed
Copy SHA
pnpm preview builds the web app and runs the production build on this machine, at http://127.0.0.1:3000, where before production refused any PUBLIC_URL that wasn't HTTPS. A loopback PUBLIC_URL now passes in production, and needs no OAUTH_PRIVATE_KEYS, since sign-in there uses the loopback client, as in development. Any other plain-HTTP URL is still refused. pnpm start stays the command a deployment runs, serving the build it was given. The README, the foundation spec's scripts, and the auth and admin settings specs describe it, with a test.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
7d258d4c
Copied
Failed
Copy SHA
useChat, in features/conversation/chat.ts, is the open conversation as a chat, in the shape of the AI SDK's useChat without a message list of its own: the branch on screen and the messages behind it from the browser store, whether they are on their way or known empty, the reply running at the end of the branch, and send, regenerate, and stop, on top of useConversation, the reply follower, and useBranch. Every turn, from a send, an edit, or a regeneration, follows its reply, puts its new message on the branch, replacing m, and calls the view's onTurnStarted, with which ConversationView scrolls to the bottom, so regenerating now scrolls there too, as sending and editing did. useStartChat sends a new chat's first message, creating the chat and reusing it after a failed send. The composer only builds the message and hands it to send, so it no longer knows the chat's key, creates chats, or posts, and BranchComposer and NewConversation pass send through. useBranch absorbs useScrollToFocus, scrolling to a message the URL focuses while a picked one keeps the scroll, so the shared page now scrolls to a linked message too. useConversationActions keeps renaming, the instructions, and syncing, and the conversation's one alert shows whichever action or turn ran last. Its tests cover sending, regenerating, stopping, the running reply, starting a chat, and scrolling to a focus, and the composer's tests check the message it hands to send. The web UI spec describes the chat.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
d9714092
Copied
Failed
Copy SHA
The browser store's React module keeps its query keys and its worker to itself and offers intents in their place: useRefreshConversation, and refreshConversation for a query's own function, check an open conversation against the PDS again as opening it does, and useSyncIndex syncs the chat index. The composer, the conversation actions, the reply follower, the chat list, and the sync status use them, so conversationRefreshKey is no longer exported and nothing outside src/store reaches the worker. The reply follower's poll now checks the conversation as opening it does, refreshing and then reconciling, so a refresh means one thing everywhere, and its key moves to reply-poll, leaving the conversation prefix to the store's own queries, whose exact invalidation on a change is commented and tested. useConversation gives waiting and empty in place of isLoading, refreshed, and refreshing, so ConversationView no longer works out when a conversation is known. The store's hooks have their own tests, and the web UI spec describes them.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
b2ec3044
Copied
Failed
Copy SHA
The convention by which a user message asks for a reply lives in turns/request.ts: userMessage builds a user record asking for its first reply, askAgain raises the attempt with any new model or effort, replyKey gives <key>.r<attempt>, and generationOf reads what a record asks for, leniently enough for one that failed validation. The runner gains send and regenerate, which write the user message and start its turn, so the turn routes only check the request and map the result to a status, and no longer need the chat services. start and answerInvalid both read the request through generationOf, in place of a cast and a parser of their own. ChatService.getMessage reads one message checked against its lexicon, which regenerate and recovery use in place of reading the raw record through chats.store. Regenerating a user message that no longer matches its lexicon now answers InvalidStoredRecord, and recovery leaves such a claimed reply as it is and clears its claim, rather than rewriting it from the invalid value on every restart. The send, regenerate, and request tests run against the runner and the module, the route tests keep the status codes, and the chat turns spec describes it.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
d467f4df
Copied
Failed
Copy SHA
The conversation:created and conversation:deleted plugin hooks run once for each conversation created or deleted, whichever client made the change, where before only the app's own routes ran them. fireChatHooks, wired in server.ts, runs them from the in-process event bus. ChatService announces the conversations the app creates and deletes, through a listener in place of its onWrite callback. Sync announces one another client created when an index sync finds a new conversationRef, whose op has no previous CID, and one another client deleted through the engine's new conversationDeleted, which the deleted index entry, the missing space, and notifySpaceDeleted all go through. RecentWrites remembers the spaces the server deleted for ten minutes, so sync noticing the server's own deletion doesn't announce it again. The first index sync after sign-in, and any while the index is empty, announces nothing. The routes no longer wait for the hooks before answering, and open tabs now hear about deletions made through the app too. The plugins spec and guide describe it, with tests and an acceptance criterion.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
fa69ac20
Copied
Failed
Copy SHA
Each account's records and blobs come from its ChatService, so createChatServices is the one place that chooses between a spaces PDS and the local fallback, for blobs as for records. The blob stores are bound to a DID like a RecordStore, as SpaceBlobStore(did, getPdsClient) and LocalBlobStore(did, dataDir), and ChatService gains putBlob and readBlob, which the attachment routes and the turn runner use in place of blobsFor, createBlobStores, turnBlobs, and the runner's TurnBlobs, all removed. The fallback's daily sweep is sweepLocalBlobs, which asks the local record store whether any record still references a blob, so only local-record-store.ts knows the local_record table. Reading or syncing a conversation and signing in no longer check the storage mode first, since the sync engine already skips accounts without spaces. Behaviour is unchanged. The turns harness runs on the real ChatService over the fake PDS in place of its fake blobs, so the test that moves a large tool output into a blob reads it back, and the blob tests go through ChatService in both modes, with a new one for a spaces blob no record references. The chat storage and attachments specs describe it.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
423a449d
Copied
Failed
Copy SHA
The reply follower, in features/conversation/reply-follower.ts, follows each pending reply in the open conversation until its final record arrives, in place of replyStreamQuery in queries.ts, useReplyStream, and the stream logic in ReplyStream, LiveAuthor, and ConversationView. useReplyFollower(skey, messages) works out the pending replies from the records and returns follow for a reply just sent, and owns the stream queries, when each stream has ended, the poll with its backoff, and the refresh once a reply finishes. A reply's views read it without ever starting a stream: useFollowedReply gives what has streamed and why the reply is no longer live, dropped or elsewhere, and useReplyStarted rerenders the header only once the reply starts. The stream query and the finished statuses are private to it, so a view can no longer start a stream by leaving out enabled: false. Behaviour is unchanged. Its tests, over a shared fake EventSource in tests/helpers, cover the stream, following before the record arrives, a dropped or unreadable stream, the poll, another server running the reply, closing, and that deltas rerender neither the conversation nor the header, in place of the poll and stream tests in ConversationView's tests and replyEvents's own. The web UI spec's streaming section describes it, and the glossary names it.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
1c31caca
Copied
Failed
Copy SHA
The README's features list adds choosing a model and thinking effort per message, with model search, custom instructions for every chat and each chat's own, handle suggestions when signing in, and a message box you can type in before the app has loaded.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
5d170c24
Copied
Failed
Copy SHA
On a reload, the composer's box takes typing before the page's code loads. The page's frame, which the server sends in place of the client-only chats, now holds the composer's frame at the foot of the panel: a plain textarea in the composer's card, with Attach, the model select's loading line, and Send disabled, so Enter only adds a line. The conversation's column shows it again while its code downloads. Each frame that gives way leaves its text, cursor, and focus in composer-handoff.ts, and the frame or composer that takes its place puts them back, so the swap can't be seen. Only a composer that starts empty takes a draft, never one editing a message. The frame and the composer are built from the same parts in composer-parts.tsx, and the composer binds its form field to MessageBox. An open chat's box says neither "Ask anything" nor "Reply" until its messages arrive, and sends nothing until then, since the message wouldn't know which one it answers. On a reload the turn skeleton in the page's frame shows at once, and the skeletons that take its place carry that on rather than vanishing to fade in again. The web UI spec describes it, with tests, including one that hydrates the server's frame, and an acceptance criterion.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
44362754
Copied
Failed
Copy SHA
Right-to-left text in the composer no longer runs under Clear. The textarea takes unicode-bidi: plaintext in place of dir="auto", so each paragraph runs in the direction of its own text while the box keeps the page's, and the room it keeps for Clear stays on Clear's side, where dir="auto" had moved it to the other side for right-to-left text. A Persian paragraph followed by an English one now puts each at its own edge. The web UI spec describes it, with an acceptance criterion, and the styling spec says a text field with a button inside it takes plaintext rather than dir="auto".
Copy SHA
Browse repository
committed
2w ago
pouriade.com
d6a00140
Copied
Failed
Copy SHA
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.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
a37a9b21
Copied
Failed
Copy SHA
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.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
b0629f6a
Copied
Failed
Copy SHA
Instructions in a chat's More menu opens a panel to edit the chat's own instructions, the systemPrompt of its info record, which the model follows in every reply there after the user's custom instructions. Saving sends them trimmed through the conversation's PATCH route and closes the panel, saving them unchanged only closes it, so the chat doesn't move up the list, and a failed save shows in the conversation's alert and keeps it open. Saving them blank removes systemPrompt from the record, as when the chat was created, since updateInfo now drops a field a patch sets to undefined. The menu item is disabled until the chat's info record has arrived from the local copy or the PDS, so the panel never starts empty over stored instructions, and the title waits for the same record, so a chat not yet on this device shows its skeleton rather than "New chat". Rename and instructions share one patch mutation, a disabled MenuItem shows as such, and the sharing panel's floating card is PanelCard, shared with the instructions panel, whose panelForm keeps a panel's form from taking room in the header's row, which had nudged its buttons aside. The web UI and styling specs describe it, with tests and an acceptance criterion.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
2354f58a
Copied
Failed
Copy SHA
The sign-in field suggests accounts as you type a handle, from Bluesky's public searchActorsTypeahead, which the browser asks itself without a referrer after a 150 ms pause, skipping a DID or PDS URL. Each suggestion shows the account's avatar thumbnail, display name, and handle, and picking one by click or with the arrow keys and Enter fills in its handle, with its avatar in place of the @ until the field is edited, while Enter with nothing picked signs in as before. The panel shows skeleton rows once a first search takes the quiet wait, keeps the last accounts with a spinner in the field while a later one loads, and says when nothing matches or the search failed, with a hint to type the full handle, and screen readers hear how many accounts were found. The login page preconnects to the typeahead and Bluesky's image CDN, the field turns off the browser's own autocomplete, and Avatar is its own component, shared with the account card. The auth spec describes it, with tests and acceptance criteria.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
1796d929
Copied
Failed
Copy SHA
The OpenAI-compatible plugin's model list sends sessionHeader too, with a fresh session ID shaped like a conversation's, since a list belongs to no conversation, and userAgent's hint and the spec say it leads the user agent, with the AI SDK's name after it on chat calls. Unticking a loaded model on a key's page takes it out of the draft, which the spec now says, along with the page's notes for a key that can't be read or isn't saved, any route answering 400 for an undecryptable key, and the Select scrolling to its active option only when it changes, each with a test and an acceptance criterion. ModelName shows a model's ID only when it differs from its name, in both the admin's and a key's list of models to tick, the admin's model rows name capabilities by their labels from the shared list, noCapabilities is typed by it, and decryptKey refuses an unreadable key in one place.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
e52f30ea
Copied
Failed
Copy SHA
A key's page lists its models in one Models card, a row each with its name, its ID when that differs, Vision, Thinking, and Tools as chips that stay pressed while on, and a button that removes it. Under it, Add models has a field to type a model ID, where Enter adds rather than saves, and, for providers that list their models, Load models, which lists them with the saved key through the new POST /api/credentials/:id/list-models, to tick the ones to use. The changes are one draft under one Save, disabled while the key has no models. The route refuses a key that can't be decrypted with a 400 asking for it again. The store refuses an empty model list or a model listed twice for every save, adding a key included, and the app's error handler turns its credential errors into a 400. The ticked list of models is a shared Checklist, used by the admin's provider page too, ToggleChip is a shared component, and noCapabilities, a key's description line, and useKeysChanged, now in settings/hooks/changes.ts, each live in one place.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
b32b1ada
Copied
Failed
Copy SHA
The app's default description, which the login page shows under the logo, is "AI chats that live on your own PDS. Use any model, and every conversation stays yours.", and its admin hint says it shows under the logo rather than the name.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
34e98bd0
Copied
Failed
Copy SHA
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.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
5cb14f93
Copied
Failed
Copy SHA
Every workspace package is versioned 0.1.0, the root included, in place of 0.0.0, and 1.0.0 for the plugin API.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
3e1b3a6f
Copied
Failed
Copy SHA
Each API key has its own page at /settings/api-keys/<id>, opened from its row, which the list now shows with how many models the key covers. Under a link back to the list, the page is titled with the key's name over its provider and base URL, and has a card for each model with Vision, Thinking, and Tools switches, the same as an admin model's, and one Save, so a user's own models can say what they can do. PATCH /api/credentials/:id replaces a key's models, refusing an empty list or a model listed twice, and leaves the key itself alone. The capability list moves to features/models for both pages to share, SectionNav takes an exact flag per section, and LinkItem fades a row on its way out.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
f41e29f8
Copied
Failed
Copy SHA
A model resolved for a conversation gets a session ID, the first 32 hex characters of the SHA-256 of the conversation's URI, so a provider can keep a conversation's calls together without learning the user's DID. ModelAccess carries it as session, and GenerateTextRequest takes the conversation to derive it from, which the titles plugin passes. The OpenAI-compatible plugin takes an optional sessionHeader that carries it, such as x-opencode-session, which OpenCode Go refuses requests without, and an optional userAgent in place of the SDK's. Both apply to the endpoint's model list too.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
d1db3422
Copied
Failed
Copy SHA
The login page shows the app's logo over its description, with the name as a heading only screen readers hear, since the logo already says the name to the eye.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
bf28924b
Copied
Failed
Copy SHA
The composer's thinking effort is its own quiet select beside the model select, between it and Send, showing the chosen level's pie and name, rather than a submenu at the end of the model list. It shows only for a model that reasons, as before. With nothing else using it, the Select loses its submenu and usePlacement its useSidePlacement. The Select also scrolls the active option into view only when it changes, since the list re-rendered on its own scroll and jumped back to the active option, so a long model list couldn't be scrolled to its end.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
e0458a33
Copied
Failed
Copy SHA
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.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
ab607011
Copied
Failed
Copy SHA
React Scan loads only with pnpm dev --scan, rather than on every development page. The server passes the flag to Vite as import.meta.env.REACT_SCAN, which the root route's head reads, and the README and CLAUDE.md mention it.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
eaa60efa
Copied
Failed
Copy SHA
The admin users list's Add role chip uses the app's Select, keeping its dashed look with a small chevron in place of the browser's arrow, and is hidden when there are no roles to add.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
9c922515
Copied
Failed
Copy SHA
The schema forms' choices, such as web search's engine, and the API keys page's provider use the app's Select, with the same arrow and open list as the theme's, in place of a native select. The form kit's SelectField takes options and a placeholder rather than option children, and since the Select can't be required as a native one can, adding a key with no provider says to choose one.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
19df6e41
Copied
Failed
Copy SHA
A secret the server can no longer decrypt, as after a lost SECRET_KEY, reads as unset and is flagged, rather than failing the page that reads it or, for an admin plugin's options, the server's startup. The settings page's web search, the admin plugin forms, and every other schema form say on each secret field that its saved value can't be read and to enter it again, and an admin can tick Clear to discard it. A BYO key that can't be read says so on the API keys page in place of its last four characters, found from its key ID without decrypting it, and a turn that uses it ends with an error asking for the key again. The unreadable value is kept until a new secret is entered, cleared, or reset, so restoring the old key in SECRET_KEYS_OLD brings it back. SecretBox gains tryDecrypt and hasKeyFor, and a value that fails authentication now throws SecretDecryptError too.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
bc89653e
Copied
Failed
Copy SHA
The login page's tagline is a weight heavier, medium rather than normal.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
a0fdb55e
Copied
Failed
Copy SHA
Load less and rerender less. The new-chat page no longer downloads the Markdown renderer: the conversation and the search results load with React.lazy, the chat route's loader starts the conversation's download, the new-chat page starts it once it shows, and opening the search starts the results', so what / downloads drops from 335 KB to 184 KB with brotli. Streamdown imports rehype-raw for its defaults, which markdown.tsx leaves out, so vite.config.ts aliases it to an empty stand-in in parts/no-raw.ts, keeping parse5 out of the build and the Markdown chunk from 163 KB to 71 KB gzipped. The logo's paths are rounded to two decimals, from 10.0 KB to 6.6 KB gzipped. The chat routes' loader starts the models, attachment types, and profile queries without waiting for them, and their head preconnects to the avatar's CDN. attachmentTypesQuery moves to its own module, so the loader doesn't bring the store's hooks into the entry. Pointing at or focusing a link preloads its route, the query cache deciding what is stale.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
c601f518
Copied
Failed
Copy SHA
Load faster. The shared page's skeleton and error page move into SharedStates.tsx, so the shared route, which loads with the app's entry, no longer brings the Markdown renderer to every page, and what the first page downloads drops from 322 KB to 155 KB gzipped. The build writes a brotli and a gzip copy beside each text or wasm file over a kilobyte, which the server sends to a browser that accepts them, so SQLite's wasm comes down at 349 KB rather than 869 KB, and files under /assets, named by a hash of their content, are kept for good with an immutable Cache-Control. The chat routes' head preloads the store's worker and SQLite's wasm, so they download alongside the app's code rather than once the worker runs.
Copy SHA
Browse repository
committed
2w ago
pouriade.com
696cc730
Copied
Failed
Copy SHA
The chat list loads 50 chats at a time, newest first, and the next page as the end of the list nears, with three skeleton rows while it comes. The browser store pages by when a chat was updated and its key, so a chat moving to the top between pages is neither shown twice nor skipped, on a new index created on open when missing, so no one downloads their chats again. Given a tag, it pages through only that tag's chats, and the tags' counts and each chat's newest reply, for the new-reply marks, come from the store across every chat rather than the rows loaded, so a chat on a later page isn't marked new when it arrives. A change in the store refetches the pages already loaded, so a scrolled list keeps its place.
Copy SHA
Browse repository
committed
2w ago
Previous
1
2
3
4
Next