Set "/seat/month" to the right of its own line
The price keeps the left edge and the unit takes the right, so the two bracket the card's width instead of stacking flush left — which left the number looking l...
The offer reaches the hero and the pricing strip
The hero had a countdown ribbon already, and it had been rendering NOTHING since the catalog moved to a percentage: `pickHeroOffer` filtered on FLAT_PER_SEAT be...
Put "/seat/month" under the price, not beside it
Three-figure rates and "/seat/month" do not share a line on a four-across card. Inline at 1rem the label either wrapped mid-text or squeezed the number it was d...
A promotion that fails to resolve must not widen to every plan
An EMPTY applicablePlanIds means "no restriction" downstream, so a restriction that fails to TRANSLATE must not be allowed to become an empty list. A Business-o...
Rebuild to pick up BillingCycle.TRIENNIAL
No source change. SecurityService clones kamo-shared-library at image-build time, and the catalog it serves is about to carry TRIENNIAL price rows. @Enumerated ...
A promo code works on its own last day
The end date is the LAST day a code works, not the first day it does not — SubscriptionPromotions.check reads it that way on the server, and the offer band's co...
A three-term commitment rail, and a promotion the page can prove
New prices: Starter $59/$49/$39 and Business $119/$109/$99 per seat per month on monthly, yearly and three-year terms. The rail replaces the monthly/annual pair...
Sell the three-year term, and stop four copies of the cycle drifting
The billing cycles were typed out by hand in five places. One of them, the plan-pricing editor, offered "BIENNIAL" — a value the server has never had — so choos...
Bill the three-year term, and quote its rate to the console
BillingCycles gains TRIENNIAL, expressed to Stripe as three YEARs rather than thirty-six MONTHs so the renewal tracks the signup anniversary — the same reason A...
A three-year term, and the promotion rules that were never read
Adds BillingCycle.TRIENNIAL so a subscriber can commit for three years. The column is varchar with no CHECK constraint anywhere, so this needs no migration — bu...
Show the report title in Send To AI, editable
The dialog let an operator edit the instruction and the description but never showed the title, and the title was not in the message either — the session at the...
Put the report title in the message, and let it be corrected
A dispatch composed the operator's instruction and the bug description with the separator between them. The title was never in it, so the session opening the me...
Carry the report title through a Send To AI dispatch
The dispatch sent the operator's instruction and the bug description and nothing else, so the one line saying what the report is ABOUT never reached the session...
Land the header's Text, Chat and Note buttons on the tab they name
The lead view keeps its tab selection in the URL — `tab` for the main tab, `commsTab` for the Communications sub-tab — and a header quick action sets both: "Tex...
Say why a number could not be opened, instead of an empty dialpad
Opening the SMS tool on a phone number resolves a conversation for it first, and that resolve can be refused — VOIPService answers "no number to text from" with...
Reply to a message, and take the reader to what it answers
MediaService has carried a reply's parent and the quote to draw for it since "send the quote with a reply, not just the pointer"; this is the half that shows it...
Stop offering an edit once somebody else has read the message
Every chat window that draws a MessageBubble — the member chat popups, and the social and ticket conversations that share ChatBox — offered Edit on the sender's...
Refuse an edit once another member has read the message
PATCH /api/media/messages/{id} was deliberately ungated on receipts, unlike the DELETE beside it. The reasoning written into it was that unsend makes read words...
Darken the xAI mark so it shows on the provider dropdown
Simple Icons ships the xAI glyph filled #FFFFFF, which is its brand color on a dark surface. The AI provider dropdown is white and the app forces light mode, so...
Offer Move on a right-clicked message, not only on a ticked one
Right-clicking a message offered Reply, Reply all, Forward, Report Spam, Archive, Label and Delete. Filing it anywhere other than Archive meant dismissing the m...
Go To Settings leads the Settings menu, above the app list
Reported with two screenshots — one org with eleven enabled apps, one with two — each circling "Go To Settings" at the bottom of the Settings sub-option menu wi...
Let a bulk move reach the folders the member made
The "Move to folder" dialog behind the selection bar was built from a literal ['INBOX', 'Archive', 'Trash', 'Junk', 'Drafts'], so the only destinations it ever ...
Never leave a blank tab when a KamoDesktop launch stalls
The tab is opened blank inside the click gesture and only navigates once a link comes back. So anything slow upstream showed as a blank white page with no expla...
The agent health preflight must fail in seconds, not twenty
health() inherited the client's 20-second default. It is a liveness check in front of every KamoDesktop launch, and when the dev machine ran out of memory on 20...
64G swap on the dev machine, and catch access that fails intermittently
The blank white page was the VM out of memory. 16Gi of guest RAM with NO swap means "out of memory" goes straight to the OOM killer: applications die mid-work, ...
Make smart folders reachable, and make clicking one work
A smart folder is a saved search that sits where a folder sits: name a question once ("unread, with an attachment") and it is one click away, current every time...
Serve smart folders and labels as messages, not as index rows
Both endpoints answered out of email_metadata.search_index and handed the client what the table holds. A smart folder returned SearchIndexEntity — messageUid, f...
Send the quote with a reply, not just the pointer
media_objs_msg.parent_id has been there all along and this endpoint has accepted parentId on the way in since threading was modelled, but a reply crossed the wi...
Make eslint actually run
`eslint .` reported nothing at all — not a clean tree, but a crash before it loaded a single file: TypeError: Converting circular structure to JSON at ...
The report form's questions move off the field's border
A member filed this against the report form itself: "What happened?" — and "What would you like to see?" on the enhancement side — had the field's border runnin...
A stamp that says which day, once the day is in doubt
Reported by a member: past chats cannot be identified or verified by the date they were sent or received — only a time is present. Their own history runs back t...
A new chat shows the exchange instead of going blank
Open a new AI chat, type a message, and the thinking bullets appeared and then nothing — no sent message, no reply, just the empty-conversation panel over a tra...
Let the sidebar's calendar checkboxes actually turn off
The checkbox beside each calendar was bound to `calendar.isVisible` and its onChange was `fetchEvents`. `isVisible` is sent by EmailService and written back by ...
Spend the site deletes before the replacements are written
Editing a report came back "The bug could not be saved: Internal Server Error" whenever the reporter left a site ticked — so every edit about the title, the des...
Leave a group chat, and keep the line that records it
Leaving is offered at the foot of the roster popover, separated from the rows above it because it belongs to the member rather than to anyone in the list, and c...
Leave a group chat, and keep join/leave lines in the transcript
Leaving is DELETE /sessions/{guid}/membership, not removeMember aimed at yourself — that route refuses self-removal on purpose. Removal is done TO somebody and ...
A join/leave row has to carry the name of who it is about
The id alone cannot be resolved in the one case these rows exist for. A "removed" or "left" line names somebody who is, by then, no longer in the session, so th...
The Favourites header must control an element that exists
Collapsing the section unmounted the list it named through `aria-controls`, leaving the header pointing at nothing. `aria-expanded` then describes a disclosure ...
Forward removedByMemberId to the browser
The relay copies named fields off ChatMessageDto onto the STOMP frame, so a field it does not copy is dropped even once the DTO declares it. MEMBER_REMOVED now ...
Declare removedByMemberId, or the relay drops it in silence
The NATS payload for MEMBER_REMOVED carries who did the removing, but the NATS→STOMP relay projects every frame through ChatMessageDto — so a field this class d...
One title rule, one group threshold, and a rename the list hears
Review of the Chats branch, four findings. Both surfaces were composing the conversation's name by hand instead of through the helpers @kamo/chat-core exports ...
No upstream error text reaches a member, whichever provider failed
memberTextFor fell through to getMessage() for anything that was not an AiProviderException. Only the HuggingFace adapter classifies its refusals; every other o...
Open on Chats, a Favourites section, and a group chat you can manage
Five requests from a member, all of them about the same gap: the Chats panel could show you conversations but gave you nothing to do with them. Chats is now th...
Name a group chat, star a conversation, remove somebody from a room
Three routes, and each refusal is a rule somebody has to be able to tell apart from an outage — so each carries its own code rather than a shared 4xx. DELETE *...
A chat can be named, starred, and have somebody removed from it
Three fields and one method, all of which the chat feature already had the shape for and none of which it could record. MEDIA_SESSION_CHAT gains TITLE. That ta...
Show why a text could not be started, not "check the number"
VoipService now answers an unprovisioned account with 409 and both a machine code (`error`) and a sentence a member can act on (`message`). apiCall read `error`...
An account with no number to text from is a 409, not a 500
Starting a text threw IllegalStateException when neither the member nor the instance had an outbound number, which Spring turned into a 500 with a stack trace. ...
Retire a model the provider won't route, and stop quoting it at members
Follow-up to the router-catalogue fix. That stops unroutable models being LISTED; this stops one already in an org's catalogue from breaking the assistant and n...
List the Hugging Face models the router will actually serve
Chat goes to the Inference Providers router **************** but discoverModels asked the HUB for models that are "warm" somewhere — a different and much larger...
Drop the security level control from the batch settings
It was a disabled Select behind a "locked" tooltip, and a chip in the collapsed summary. Both reported a value the admin never chose: security level is derived ...
Like what you see shipping?
Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.