- a live app that fails to move stops the address change and removes the new address
- an app lookup that fails is an error, not "no app"
- moving an app writes the new record first and undoes it if Puter refuses
- an app whose record can't be saved is deleted again
- a recreated preview takes the old preview's app and keys
- republish copies keys before switching the live site
- renames of one site run one at a time
- the not-found hint matches the project's kind
- tools carry no Builder-only fields to the AI
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* improve loading messages
* analytics
* drop the running build on stop, and don't count send now as abandoned
stopActiveTurn clears the build right away, so a Stop or a closed tab while
Send now's message is still being set up no longer reports the replaced build
and its wait time. Send now itself reports nothing: the user is still
building, only something else.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
* add message queueing
* fix message queue review findings
- a stopped turn that fails after the next one started shows no error card
- a message stopped during its upload always comes back to the box
- a clarify reply keeps queued messages in the draft
- a send clears only the files it sent from the tray
- a send takes its picked element when it starts
- test-pwa passes stop_svg
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
* various performance improvements
* address review on the performance improvements
- the checkpoint after a turn's last round saves it as finished, so a
reload before the end-of-turn save lands doesn't offer Resume
- only earlier turns' SuggestNextSteps calls are left out of requests, so a
call in this turn keeps its error and a resumed turn ends on a result
- the early preview clear starts only when a refresh is pending, not after
a failed write or publish_site's own refresh
- fix the snapshot comment and the comment merged into CUT_OFF_TOOL_ERROR
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
* save the pending upgrade click so Upgrade Completed survives a reload
The source of a free user's Upgrade click lived only in memory, so a reload
before the user came back (a phone discarding the tab while they paid, a
closed tab, a restarted app) lost both the Upgrade Completed event and the
"You're subscribed" dialog. The click is now saved per account in
localStorage for 24 hours and checked on page load, on focus and on return
to the tab, under a per-account Web Lock, the same way Upgrade After Limit
works.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* report how long after the click an upgrade completed
Upgrade Completed now carries since_click, bucketed <10m, 10-60m or 1-24h
like since_limit on Upgrade After Limit, from the time saved with the click.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* keep the clicking tab's upgrade in memory and wait for startup
Review fixes for the saved Upgrade click:
- The tab where the click happened keeps it in memory again, so it still
turns paid and shows its success dialog (with Continue building) when
another tab credited the saved click first. It isn't counted twice.
- When storage doesn't take the click, that copy is counted instead, as
before this branch.
- Focus checks wait until startup has reopened the project from the link,
so the success dialog never opens over the loading screen without that
project's next step.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
The two lookups ran one after the other for every backend on every
publish, duplicate and delete. They're independent, so they now go out
together, with the same meaning for every failure.
When going back to the default name reached the preview but not the live
backend, the note said publishing again would fix it. Publishing never
carries a default name over, so the live site kept the old name for good.
The note now says to call the tool again.
Line breaks are accepted and become spaces. The refusal fires only for @,
quotes, backslashes, angle brackets and other control characters, so it
now says that.
The refusal counted the raw input, but the check measures the name after
runs of whitespace collapse, so an 81-character name could be reported as
100 characters.
Email a Builder backend sends with puter.email.sendTransactional is labelled
with the title of the worker's sandbox app, which starts out as its internal
name, so recipients saw "sandbox-<worker>" as the sender.
- New set_email_sender_name tool renames the worker's sandbox app (the
preview's backend, and the live one when the site is published). Names the
email service would mangle (@, quotes, over 78 chars) are refused.
- Publish copies a chosen name to the live backend; Duplicate copies it to the
copy's backends; Delete, retiring a live backend, and a discarded duplicate
put the default back first, since Puter reuses sandbox apps by name.
- A backend on its default name never triggers a rename or a warning, and a
live name set by hand in the Dev Center is never overwritten.
- Prompt tells the AI to set a sender name whenever a worker sends email and
to pass replyTo when recipients should be able to reply.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* add compaction
* address compaction review
- Send the summary as the first block of the assistant turn after it, the
shape Puter documents, instead of as a message of its own.
- Use a failed round's summary once anything follows it. It's complete, and
skipping it paid for a second one. Only an entry with nothing after it is
left out, so a request never ends on it.
- A Claude summary is plain text in Puter, so a project moved to GPT or Gemini
gets it as a text message instead of the full history.
- Cut the history before copying it, and never change the stored history.
- Keep two summaries from one stream in order.
- A usage line with no input counts no longer resets the request size.
- Drop the unused turnUsage.lastInput.
- Replace the test for a chunk order Puter never sends (compaction after the
usage line) with tests for the above.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
#118 changed the low-balance override to pass this.upgradeUrl to
openLimitModal, but the source check in test-usage-limit-unblock.mjs
still expected a no-argument call. The failing check made prebuild
(npm test) exit 1, blocking the build.
Claude-Session: https://claude.ai/code/session_01FWCxozU1v7Zb8DhLpKBQcd
Co-authored-by: Claude <[email protected]>
On iOS the keyboard doesn't shrink the layout; Safari pans the visual
viewport to reveal the focused field. The composer sits at the bottom of a
fixed-height column, so it stayed visible only while that pan held, and
scrolling the conversation, the composer growing, or a streaming reply
could drop it behind the keyboard until it was dismissed and reopened.
While a field in the chat column has focus on a touch device at 1x zoom,
size the app to visualViewport.height and undo Safari's pan. Fields
elsewhere (dialogs, sidebar search, the preview iframe), desktop and
pinch-zoom are untouched.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* ai selector feture
* address review on the ai selector
- run a reply's tool calls only once the reply has ended (its usage line or
the end of the stream), never after a pause: a healthy reply can be silent
while its next call is written, so running early let a stale call land
after newer work
- simplify MCP schemas for GPT and Gemini only where a schema holds other
schemas, so parameters named like keywords (definitions, $id, type) and
example or default values are kept as they are
- Resume restores the last checklist of a batched reply, like the
end-of-turn backstop
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
* add code editor
* address code editor review
- keep text typed while saving before leaving, publishing or restoring
- keep a copied backend's hand-edit note and guard under its renamed file
- ask before leaving with edits to a file deleted elsewhere
- carry the guards of files moved before a move fails
- lift the overwrite guard only after a complete read in a live turn
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* fixes
* remove copy
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
The provider call can't be cancelled, so after Stop (or deleting or leaving
the project) a generation with save_to_project still copied its file into the
project when it arrived: possibly in the middle of a publish, or recreating
the folder of a project that had just been deleted. The file now stays in the
chat's media folder and the result says it was not saved into the project.
removeCard took a failed version's tile down but left the grid's count, so
four options with one failed were announced as "4 generated images" and
laid out differently from the same grid after a reload, and a lone image
left in a grid showed at half width. The grid is now recounted the way a
reopened chat shows it, and a single remaining image gets a full-size card.
The media folder was copied before the copy's chat file was written. When
that write failed, the cleanup removed the copied project files, workers and
site but not the media copy, which was left behind for good.
A failed copy into the project (save_to_project) threw out of
runGeneration after the file was generated and saved: the card stayed on
"Generating image…" for good, no analytics event was sent, and the image
the user paid for was lost. Now the card shows it, and the note tells the
model the project file does not exist and how to copy it in.
Port GenerateImage, GenerateVideo and GenerateSpeech from ../ai, with
their chat cards (js/media.js): pending card with a timer, image grid for
several versions, full-screen image viewer, Download / Open, and replay
of past generations when a chat is reopened.
- generated files live in a per-chat .media/<chatId> folder beside the
project, so version snapshots and restores never touch them; deleting
a chat removes it, duplicating a chat copies it
- save_to_project puts a file into the site (counts as a project change);
copy and ViewImage can read from the chat's media folder
- system prompt: custom imagery for sites (max 4 images per turn), video
only on explicit request, speech only (no music or sound effects)
- Plausible events: Image/Video/Audio Generated and ... Generation Failed,
one per request, with the request's options and a failure reason;
prompts and file names are never sent
- tests: generated-media (incl. analytics) plus ai's card, lightbox and
read-URL tests; path-scoping and load-media-urls updated
* better limit screen
* limit screen: checklist card, builder animation, clearer wording
- steps shows the build's checklist instead of a bare progress bar
- no screenshot shows a looping chat-and-preview animation
- live address sits in an address bar over the screenshot, badge note gone
- "stopped partway" only when the turn did work, otherwise app/new
- every version has Not now and Upgrade
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* limit screen: review fixes
- Upgrade saves the project the dialog opened for, not the one on screen
- Continue building re-checks the project before resuming
- checklist icons say Done / Not done to screen readers
- darker grey for unfinished steps (#666670, 5.3:1)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
* usage limit analytics
* Count calls and cost of earlier rounds when a later call fails
Puter sends a call's usage chunk last, after its tool_use, and the stream
runs the tool round (opening the next call) as soon as it reads the
tool_use. When a later call was refused, every earlier stream was still
parked in its tool round with its usage unread. So Usage Limit Hit
reported calls: 0 for every mid-build refusal, and the build's cost never
reached the chat's saved total, which made Project Published's cost_band
read low for projects with a cut-off build.
Each call is now counted at its first tool_use or its usage, and a parked
stream reads its remaining usage before unwinding, under one 1.5s deadline
per turn so a dead connection can't stall the unwind once per round.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Add cost band to Project Published and Usage Limit Hit
Adds cost_band (dollar bands, finest under $1 where the free-credit
allowance is decided) and whole-cent cost_cents to Project Published,
and the same pair to Usage Limit Hit, so the spend of users blocked
before publishing can be compared with what a first publish costs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KuLWkaCG7cviLYZ1tkb7T8
* Drop cost from Usage Limit Hit; center cost bands on the $3 allowance
At a limit hit the project's cost is roughly the free allowance minus
spend elsewhere, so it adds nothing. Rebalance cost_band to be finest
between $1 and $5, around the current $3 allowance.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KuLWkaCG7cviLYZ1tkb7T8
---------
Co-authored-by: Claude <[email protected]>
* enable removal of puter badge for user with active plan
* Address review: confirm project switch, tighter tag match, fresh plan check, skip card for stopped turns
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Track project context on Usage Limit Hit
Adds published, any_published, builds_completed (counted from the chat
history) and projects to the Usage Limit Hit event, so limit hits can be
split by whether the user had a successful build, a published app, or
earlier projects.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QBPTrbgLwvA1thzXcMauNS
* Widen announce test window for the longer Usage Limit Hit call
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QBPTrbgLwvA1thzXcMauNS
* Don't count version-restore notes as completed builds
A restore appends a plain assistant note to the chat; after a failed or
interrupted build it became that build's last reply and inflated
builds_completed. Flag the note (versionNote, stripped before the AI
call) and skip it, matching older unflagged notes by their text.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QBPTrbgLwvA1thzXcMauNS
---------
Co-authored-by: Claude <[email protected]>
* Track token usage and first-publish flag on Project Published
Adds the project's accumulated AI usage (input/output/cache tokens, a
total, a coarse band for dashboard grouping, and cost in cents) plus an
explicit first_publish boolean to the Project Published event.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KuLWkaCG7cviLYZ1tkb7T8
* Test the Project Published analytics payload
The other publish tests don't load the usage helpers, so they only ever
exercise the fallback event. Drive the real doPublish with the helpers
loaded and check token totals, band, cost, first/republish flags, the
zero-usage band, the fallback, and tokenBand boundaries.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KuLWkaCG7cviLYZ1tkb7T8
---------
Co-authored-by: Claude <[email protected]>
* twitter player card template
* fix player card setup chip and FAQ, test template instructions end to end
The setup chip and the FAQ still named a your-site.puter.site placeholder the
page no longer has, and the FAQ promised an address check that was removed.
Add a fork test that follows a template's instructions from template.js into
the new project's system prompt.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Add official project templates: copy a finished app into your account
Templates are small, complete apps that ship with the builder
(src/templates/<slug>/). "Use this template" copies one into a new
project in the user's own account, where it is theirs to keep building on.
Where they appear:
- A "Start from a template" row under the landing hero (shares the
community feed's grid and layout), opening a template dialog.
- A static page per template at /templates/<slug>/ and a /templates/ hub,
rendered by the existing SEO page pipeline (sitemap, llms.txt, OG cards).
"Use this template" signs the visitor in inside the click, parks the slug
in the same-origin handoff store, and goes to /?template=<slug>&handoff=…,
where the app copies it without asking again. A bare /?template= link
only opens the dialog.
The fork is an ordinary project:
- files are written into a fresh app directory, with its own draft preview;
- each worker a template ships is deployed under a fresh account-unique
name from <appDir>/workers/<name>.js, so the project owns it (deleting the
project deletes it); the frontend refers to it only as {{WORKER_URL:name}},
replaced with the deployed URL;
- the system prompt carries a note that the app already exists (file list,
preview, backend by path and URL, so Make a copy rewrites it correctly);
- it opens with the template's suggestion chips, a "Started from" note, and
a version-history restore point.
Any failure removes everything it created, workers first.
The build validates every template (layout, placeholders, the app's own
index validator) and ships /templates.json plus versioned files under
/template-files/<slug>/<version>/. Behind FEATURE_FLAGS.templates.
Ships three templates: Feedback Board (with a serverless backend),
AI Chat Assistant, and Product Landing Page.
* Reject protocol-relative thumbnail paths in the template index
The index validator accepted //host/x.png as a same-origin path, which the
landing card would then load from another host.
* Decode template dev paths only once they are ours
The templates dev middleware decoded every request path before checking its
prefix, so a malformed escape anywhere threw from our code. It now decodes
only its own paths and answers a malformed one with 400.
* Hide the landing sections under the project loading overlay
body.project-loading hides the landing hero so nothing ghosts through the
overlay's fade-in, but not the rows below it, so the templates row (and the
community feed) showed through while a deep-linked project was opening.
* Keep the space after a template chip prompt that ends in a colon
Chips like "...match my brand: " put their prompt in the composer as is,
for the user to finish. The index sanitizer trimmed the trailing space, so
the first word typed ran into the colon.
* Use a div for the templates hub card body
It holds the card's heading and paragraph, which a span may not contain.
Rendering is unchanged (the element was already display: block).
* Make the landing template cards links to their pages
The cards were buttons wrapping block elements (not allowed in a button),
and could not be opened in a new tab or copied. Each is now a link to
/templates/<slug>/: a plain click still opens the template dialog, while
modified and middle clicks keep their usual link behaviour.
* Re-enable Use this template when Back restores the page
Back from the app restores the template page from the back/forward cache
mid hand-over, so both buttons stayed dimmed and ignored every click. The
page now clears its busy state on a persisted pageshow.
* performance improvements
* suggested changes
---------
Co-authored-by: Reynaldi Chernando <[email protected]>
A publish runs for several seconds and its progress note invites the user
to keep building meanwhile, so switching to another project and back
mid-publish is ordinary use. refreshPublishButton bailed outright while a
publish of the open project was running — right while the user stayed put
(setPublishBusy had painted the button), wrong when they came back: the
button kept the label and state painted for the project visited in
between ("Published" on a never-published project), enabled, until the
publish finished. It now paints this project's own state and holds the
button disabled as "Publishing…", and still leaves the popover alone.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
A composer send abandoned before its message reached the conversation
(Stop while the attachments were still uploading) hands the text back to
the composer, since the send had already cleared it and tombstoned its
draft. Whether the message had reached the conversation was judged by the
history having grown past the turn's start — but repairDanglingToolUses
also appends to it, synthesising results for a build that was cut off
mid-tool. In such a project (a common state: refresh mid-build, then type
the next instruction) an abandoned send counted as "in the conversation":
the hand-back was skipped, the text was gone, and Resume re-sent the
repaired old history instead. A flag set where the message is pushed now
decides it.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
puter.ui.alert / prompt, the context menu and the pickers are custom
elements appended to <body>, outside every panel of ours, and the buttons
in their shadow roots don't stop their clicks: a press on OK / Yes / No
bubbles to document with the dialog host as its target. Every "click
outside closes" handler read that as a click outside and closed the
surface the dialog had been raised FROM — a failed publish's alert closed
the publish popover and dropped the typed address it had just promised to
keep (the next open showed a fresh random name); an invalid address in the
Settings dialog collapsed the sidebar under it, so the dialog's focus
return landed on nothing; answering "No" to leaving a running project
closed the project list the user was browsing. The delete flow alone was
guarded against this, with its own flag.
One predicate, isInPuterDialog, now covers every outside-press closer: the
publish, share, device, issues and versions popovers, the projects sidebar,
the account panel and the overflow menu.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
metaContent located the tag with a pattern that stopped at the first ">",
including one inside a quoted attribute value, so a description like
content="a > b" came back empty and the manifest shipped without one.
The tag now runs to the first ">" outside a quoted value.
Co-Authored-By: Claude Fable 5.1 <[email protected]>