Repository navigation
Publish from chat - #149
Merged
Merged
Conversation
When the user asks the AI to publish, it calls the new OfferPublish tool, which puts a Publish card in the chat. The user publishes with one click; the AI can never make a site public on its own. The card draws the project's publish state, like the toolbar button: an address field and Publish before the first publish, Publish changes when the live site is behind, and Live with the link once it's up to date. Its button waits while a turn runs, and errors show inline beside the field. doPublish is split: publishProject(ui) holds the whole publish (guards, busy state, release, hosting, records, analytics), and the popover and the card each pass their own render/start/success/fail hooks. The popover's behavior is unchanged. A publish from the card is tracked with source: 'chat'.
Only the newest Publish card in the chat is a control now. An older card stops following the project's state: one the user published from stays as a "Published" line with the address it went to (saved on its tool result as publishCardUrl, never sent to the model), and one that wasn't used is hidden, since a newer card replaced it. A publish, from the card or the toolbar, now adds a hidden note to the conversation saying the site is live and where. Before, the model's last word on it could be the OfferPublish result saying nothing is public yet.
The AI can now offer an address change: when the user asks, it calls the new OfferAddressChange tool, which puts the Publish card in its Change address mode in the chat, with the new address (editable), the address it moves from, and a note that links to the old one stop working. The user moves the site with one click; the AI can't do it on its own. The popover's address change is now changePublishedAddress in ui.js, used by the popover and the card. It also: - redraws the toolbar button and the chat cards, which kept showing the old, deleted address - leaves a hidden note telling the AI the site moved and the old address no longer works - waits while a turn or a publish runs (the note can't land mid-turn, and a publish would update the address being moved), and a publish waits for it An older card that published to an address since moved shows that address as plain text, no longer a link to a site that is gone.
A message sent while the published site was moving to a new address
started a turn, and the change's note for the AI ("the site moved, the
old address no longer works") then landed in the middle of that turn,
which also worked with the old address. The change only checked for a
turn already running when it began.
A send now waits for an address change of its project, with a toast, the
same as it waits for a publish. The address change records which project
it is for, so a send in another project doesn't wait.
A publish with unsaved edits in the Code view saves them before it starts, and the popover's Change address could run in that moment: the address change only waited for a publish already copying files. The two then overlapped, so the publish could update the old address after it was deleted, or retire the release the new address had just been pointed at. The address change now also waits for a publish that is still saving.
The card redraws its markup when a publish starts and when it finishes. That replaced the focused Publish button, so focus dropped to the page: a keyboard user who pressed Enter on Publish was left with nothing focused, and their next Tab started from the top of the page. When focus was in the card, a redraw now gives it back: to the address field if that had it, else the button, or the site's link once it's live. While the button is disabled for the publish, the card itself holds focus (no focus ring, like the Publish popover). Focus anywhere else is left alone. The test DOM now moves focus to <body> when the focused element is removed, as browsers do.
A failed publish or address change from the card always focused its address field. Publishing takes a few seconds, and the user may have started typing their next message meanwhile: the failure moved their cursor out of the message box mid-sentence. The error still shows beside the field and is read out, but the field only takes focus from the card itself or from nowhere.
A publish or an address change from the card shows how it went on that card. When the user opened another project while it ran, the card was off the screen, so a failure told them nothing (the popover alerts wherever the user is). And a user who came back mid-publish sees a new copy of the card, drawn by loadChat: the outcome went to the old copy, so the error never showed, and the card later read as unused. The outcome now goes to the card on screen, found by its id. When that project isn't open, a failure shows as an error toast with the reason.
The card's address field was 13px. On iOS Safari a field under 16px zooms the page when it's focused, and the page stays zoomed after. Phones also draw the .puter.site suffix next to it at 16px, so the field and its suffix didn't match. It joins the other address fields in the phone rule that sets 16px.
On a phone the card's address field shared its row with the button, and was so narrow that a longer address was cut off right against its .puter.site suffix: while publishing, "bakery-orders" showed as "bakery-ord.puter.site", and an address change to "sweet-bakery-orders-online" showed as "sweet-t.puter.site", each reading as a different address. The card is now a size container. Under 440px wide, a row with an address field wraps: the field takes the whole row and the button spans the card under it. Wider cards, and the rows without a field, are unchanged.
The Change address card only sent "Site Address Changed" when its project was still open at the end. A user who opened another project while the change ran moved their site, but it wasn't counted. The popover counts it either way. The card now does too.
The card's inline error ("That address is taken. Try another.") was read
out once when it appeared, but nothing tied it to the field: a screen
reader user who came back to the field heard no error. The same for the
Change address card's warning that links to the old address stop working.
The field now has aria-invalid while it has an error, and
aria-describedby pointing at the error and the warning. Typing a fix
clears aria-invalid along with the error.
The toolbar's and the chat's address change wait for a rename from the Settings dialog, which marks the project as publishing. The other way round there was no check: renaming the open project in Settings while its address was changing from the toolbar or the chat ran both. Each created a new address and dropped the old one, so one of the two new addresses was left registered and unused, counting against the account's limit. The Settings rename now waits for it, with a toast.
The Publish card and publishProject waited while any project's address was changing. A change keeps running when the user opens another project, so that project's Publish waited for nothing of its own, and an unused Change address card there showed "Changing…" for the other project's change. Both now ask about the open project only, as the send does.
changePublishedAddress refused while any project's address was changing. A change keeps running when the user opens another project, and that project's Change address card is ready (it waits only for its own project's change), but a click there failed with "Wait for the current task to finish". The changes in progress are now kept per project: a project can't start a second change of its own, and others aren't held by it.
A failed publish left its error on the card's data, and only typing in the field cleared it. A failed Publish changes, then a publish from the toolbar that worked, drew the card as Live, which shows no error. The next change the AI made drew it as Publish changes again, with the old "Couldn't publish" under it, as if that had failed too. An error now goes when another publish starts or the card moves to another state. A redraw in the same state still keeps it.
The field's wrapper turns its border blue when the field has focus, and the app's global input:focus-visible ring, which text inputs match on any focus, also drew a 2px box around the bare input inside it: two rings, one cutting through the address next to its .puter.site suffix. The wrapper is now the only indicator, with its border and a soft ring around the whole address (as the delete dialog's field does).
A publish that first saves the Code view's edits, or a turn starting, disables the card's button without redrawing the card. The browser moves focus off a button once it is disabled, to the page, so a keyboard user who pressed Publish with unsaved code edits was left at the top of the page, and their next Tab started there. The card now takes focus before its focused button is disabled, as it already does when a redraw disables it.
The toolbar's and the chat's address change leave the AI a hidden note
naming the old and the new address, since nothing else tells it where the
site is. The Settings dialog's rename moves the site the same way but
left none, so the AI kept the old address, which no longer works, and
gave it to the user ("your site is at ...").
The rename now leaves the same note: in the open conversation, or, for
another project, in its saved one, written with its new address
(savePublishedFields takes the note). Like the other two, it waits for
the open project's turn or restore, so the note never lands in the
middle of a turn, between a tool call and its result.
A publish or a chat address change keeps running when the user opens another project, and its new address is then saved to the project it was made for. The hidden note for that project's AI was only added while it was open, so it was dropped: its AI still went by its last word, "nothing is public until they click Publish", or the address that no longer works. The note now goes into that project's saved conversation, with its new address (savePublishedFields).
A publish or an address change from a card is saved on its tool result, so the card reads as used after a reload. It was only saved while the project was still open, from the open conversation: - an address change that ended once the user had opened another project wasn't saved at all. Back in the project, the Change address card read as unused and offered to move the site to the address it already had. - a publish that ended there wasn't either, and one that went live while the project was open but was left during its last save wrote the mark into the other project's conversation, where it found nothing. The mark now goes into the card's own project: the open conversation, or else its saved file, under the chat file's lock. publishProject tells the card about a publish that ends in another project (its new away hook), as it already tells it about one that ends in this one.
A publish or an address change adds a hidden note for the AI at the end of the conversation. A turn the user stopped between tool calls can leave a call there with no result, which the next send fills in at the end (repairDanglingToolUses). With a note in between, that result landed after the note, splitting the call from its result: a request the model API rejects, and every send after it, since the history stays that way. Stop mid-build, then Publish, was enough. The missing results are now filled in before the note goes in (in the open conversation, and in a saved one through savePublishedFields).
A copy of a published project starts unpublished, but it inherited the notes that told the AI the site is public at the original's address, so the copy's AI gave that address out as the copy's. The copy now gets a note saying the address is the original's, and the Publish cards lose the original's publish record, so none reads "Published" in the copy.
Saving the site's address unchanged closed the editor on main, whatever was running. The new waits for a turn, a restore and an address change ran before that check, so during a build it was refused with "Finishing the current task" and the editor stayed open. The unchanged address is now checked first, as the popover does. The test's stand-in for the editor's redraw referred to a name the sandbox doesn't have, so it threw where it was called; it now counts.
OfferPublish should be the last call of a reply, but a model that calls it before an edit was told the site was up to date with nothing new to publish. It said so to the user, and once the reply ended the card offered Publish changes. A change still to run in the reply now counts, so the AI is told what the card will offer.
An older Publish card crosses out the address its publish went to once the site has moved. It was #8a8a8a on the card's #fafafa, 3.3:1, under the 4.5:1 that 13px text needs. It now uses the #6b6b6b of the "Published" label beside it (5.1:1). Dark mode was already 5:1.
When Change address in the popover is refused during a turn, its "Finishing the current task" toast had no key or throttle, so each press of Save or Enter stacked another copy. It now shares the publish-blocked key and 3s throttle of the other waits.
On a 320px phone the card's field fits about 25 characters at 16px, and an address the AI suggests can be 30. It was cut mid-letter against .puter.site, so "neighborhood-bakery-orders" read as "neighborhood-bakery-order.puter.site", another address. The field now ends a cut address with an ellipsis; focused, it scrolls as before.
The card arrives during the AI's reply, so its button starts out waiting. Enter in the address field did nothing, and the dimmed button gave a keyboard or screen reader user no reason, where the popover shows "Finishing the current task". Enter now shows the same throttled wait toast, naming the turn, restore, address change or publish it waits for.
Once the site moves, an older Publish card crosses out the address its publish went to. The line-through was the only sign, and screen readers don't read it: the card was heard as "Published x.puter.site", which sounds live. A visually hidden "(no longer works)" follows it now.
In the chat beside the preview, a card's link is cut off with an ellipsis (at about 250px), and a mouse user had no way to see the whole address. The links and the crossed-out address now carry it as their title, as the popover's link does.
A chat keeps the system prompt it started with. One from before this change sends the user to the preview's Publish button and never names OfferPublish or OfferAddressChange, so in the projects most ready to publish, the AI had only the tools' descriptions to go on. Such a prompt now gets the new sentences as a note, the way the required opening line reaches older chats.
publish_site's description told the AI never to share a public link, and the notes left after a publish or an address change only said where the site now is. Asked "what's my link?" after publishing, the AI had two rules that disagreed, and could hedge or point at the preview. The description now keeps the link private until the user publishes, and each note says the AI can give the user the address.
Only the Publish panel in the preview toolbar has the "Remove the 'Made with Puter' badge" box; the Publish button in the chat publishes without it, keeping the badge. OfferUpgrade told the AI to say "open Publish", which now reads as either button, and a subscriber asking to publish without the badge could be offered the chat's button and go live with it. OfferUpgrade now names the toolbar's Publish, and OfferPublish's description says its button has no badge option.
OfferPublish and OfferAddressChange made the address they were given the way a title is made into one: dots became hyphens and it was cut at 30 characters. "Move it to joes-bakery.puter.site" offered joes-bakery-puter-site, a longer address lost its end, and the AI presented the result as what the user asked for. An address given with https:// or .puter.site now drops those, and a valid one is kept whole (up to 63 characters, as the popover takes). Anything else is still made into an address.
A publish ships the preview's files. In a project published once and then opened without a preview, OfferPublish still put a card in the chat and its Publish changes worked, shipping the whole project folder (publishedRootDir falls back to it). The toolbar has no Publish button then, and Settings refuses. OfferPublish now needs the preview, as its error already said, and a card from before waits, with Enter saying there is nothing to publish yet.
'Site Address Changed' is new here. The chat's card sent it with source 'chat' and the popover with no source, while the Settings rename didn't send it at all, so the event fell short of the address changes made. The popover now sends 'toolbar' and Settings sends 'settings'.
A copy of a published project starts unpublished, so its newest Publish card is drawn as a Publish form, filled in with the address the AI suggested for the original. That address is taken by now, so its Publish could only fail with "That address is taken". The copied cards' addresses are now cleared, and a publish from the empty field names the site from the project, as the popover's does.
Mutating the code showed these passed with it broken: - the address change's refusal while a publish runs, and its refusal when the published files are missing (nothing ran either case); - the publishCardUrl strip, checked only by reading the source: it now runs prepareHistoryForAI; - a busy refusal's words on the card, where any error passed. The loadChat check also pinned its arguments' order, which a harmless reorder broke; it now checks each argument.
The Settings rename's test always said no address change was running, and only read the source for the wait's toast: with the return after that toast removed, the rename went ahead and the tests still passed. It now runs the rename while the project's address is changing and checks that nothing moves.
Two of the publish's rules passed their tests with the code broken: - it waits while this project's address is moving, checked only by reading the source; - a failure clears the busy state before the surface shows it, which every test stubbed out (shown while busy, the card's redraw wiped its error). Both now run the real publishProject.
Its file may not have the published address yet, so the copy reads it from the live state. Removing that read passed every test.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #147.
What it does
Users can now publish from the chat. When they ask the AI to publish ("put it online", "make it live"), it calls a new OfferPublish tool, which puts a compact Publish card in the chat. The publish only happens when the user clicks it, so the AI can't make a site public on its own, or because a web page or tool result told it to.
The card shows the project's publish state, the same as the toolbar button:
[bakery-orders].puter.sitePublish (the address is editable; the AI suggests one)bakery-orders.puter.sitePublish changesbakery-orders.puter.siteRefactor
doPublish()is now the popover's thin wrapper around a newpublishProject(ui), which holds the whole flow: guards, saving unsaved code, busy state, release staging, hosting, records, the key note and analytics. Each surface suppliesrender/desiredName/start/success/failhooks:Project Publishedevent carriessource: 'chat'. The popover's event has nosource, as before.Other changes
publish_sitedescription now send a publish request to OfferPublish. Everything else about the preview stays as it was.PREVIEW_SAFE_TOOLS(it writes no files). It isn't strict, which keeps the strict-tool count under Claude's limit of 20.Tests
scripts/test-publish-card.mjsruns the real card code in a mini DOM. It covers each state, waiting during a turn, keeping the typed address, click/Enter, busy, errors, success, stale turns and reload, plus the wiring.test-publish-analyticsdrives the realpublishProjectwith card hooks: the hook order,source: 'chat', and a failure that callsfailwith no alert.doPublishso they also loadpublishProject, and updated three text assertions that pinned the old line layout.npm testpasses andvite buildsucceeds.Review fixes
Each fix is its own commit, with a regression test that fails without it.
Reliability
Publish card UX and accessibility
.puter.site("bakery-ord.puter.site"). Below 440px the button now goes under the field (container query). Wider cards are unchanged.aria-invalid,aria-describedby).Analytics
Second review round
Each fix is its own commit, with a test that fails without it.
What the AI is told
Publish card
awayhook for this.npm testpasses andvite buildsucceeds.Third review round
Each fix is its own commit, with a test that fails without it.
What the AI is told
Regressions
UI
npm testpasses andvite buildsucceeds.Fourth review round
Each fix is its own commit, with a test that fails without it.
What the AI is told
https://and.puter.siteare dropped and a valid address is kept whole.Reliability
Publish card UX and accessibility
.puter.siteand read as a different address. It now ends with an ellipsis.Analytics
Site Address Changed. Every address change now sends it, with sourcetoolbar,chatorsettings.Tests
Mutating the code showed several tests passed with it broken, and they now catch it:
prepareHistoryForAI;publishProject;A text check that pinned argument order now checks each argument.
npm testpasses andvite buildsucceeds.