diff --git a/README.md b/README.md
index 357bf3b..ea443d7 100644
--- a/README.md
+++ b/README.md
@@ -225,7 +225,7 @@ wipes the mock session's scratch store. Share previews are out of scope;
```bash
cd ./my-package
-kody dev # serve kody.app.entry; hot reload; --target mock
+kody dev # serve kody.app.entry; live reload; --target mock
kody dev fetch /health # agent-friendly HTTP probe
kody dev job heartbeat # run a declared job once (never schedules locally)
kody dev webhook ping --body ./fixture.json
@@ -261,6 +261,13 @@ kody search --target mock local
- **Logs**: app `console.*` output streams to the terminal tagged with the
request, e.g. `[GET /scores #12] saved 3 rows`. `warn` and `error` go to
stderr.
+- **Live reload** (on by default; `--no-live-reload` turns it off): HTML
+ responses get a small ``
+
+const keepAliveMs = 15_000
+
+export type LiveReloadMessage =
+ | { event: 'reload'; data: { buildId: number } }
+ | { event: 'build_error'; data: DevErrorView }
+ | { event: 'runtime_error'; data: DevErrorView }
+
+export type LiveReloadHub = {
+ /** Serve the SSE stream; sends `hello` with the current build first. */
+ handleEvents: (req: IncomingMessage, res: ServerResponse) => void
+ broadcast: (message: LiveReloadMessage) => void
+ clientCount: () => number
+ close: () => void
+}
+
+export function createLiveReloadHub(input: {
+ state: () => { buildId: number | null; error: DevErrorView | null }
+}): LiveReloadHub {
+ const clients = new Set()
+ const send = (res: ServerResponse, event: string, data: unknown) => {
+ res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
+ }
+ const keepAlive = setInterval(() => {
+ for (const res of clients) res.write(': keep-alive\n\n')
+ }, keepAliveMs)
+ keepAlive.unref()
+ return {
+ handleEvents: (req, res) => {
+ res.writeHead(200, {
+ 'content-type': 'text/event-stream; charset=utf-8',
+ 'cache-control': 'no-store',
+ connection: 'keep-alive',
+ })
+ res.write('retry: 500\n\n')
+ send(res, 'hello', input.state())
+ clients.add(res)
+ req.on('close', () => clients.delete(res))
+ },
+ broadcast: (message) => {
+ for (const res of clients) send(res, message.event, message.data)
+ },
+ clientCount: () => clients.size,
+ close: () => {
+ clearInterval(keepAlive)
+ for (const res of clients) res.end()
+ clients.clear()
+ },
+ }
+}
+
+/** Insert the client tag before ``, else `