diff --git a/README.md b/README.md index b9c2775..be9efc3 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Review a page running on localhost: /human-review (localhost URL) ``` -Human Review opens the file in your browser. Make direct edits, leave comments, and click Send. Your agent receives all your feedback in one batch, updates the source, and refreshes the page for another review. +Human Review opens the file in your browser. Make direct edits, leave comments, and click Send. If it's already fine, click **No change** — or just close the tab. Either one tells the waiting agent to stop polling instead of hanging until timeout. Your agent receives feedback in one batch, updates the source, and refreshes the page for another review. Note: For HTML files, direct edits and resizes save automatically. For Markdown and localhost pages, click Send so your agent can apply them to the source. @@ -63,6 +63,7 @@ Note: For HTML files, direct edits and resizes save automatically. For Markdown - **Remove elements** without explaining the deletion in chat. - **Command-click links** to review multiple pages without losing your feedback. - **Send every edit and comment at once** instead of writing a long chat message. +- **No change** (or closing the tab) ends the review so the agent stops polling. I use Human Review to edit AI-generated plans, update landing pages, review localhost apps, and remove the extra copy AI likes to add to UX. diff --git a/src/SKILL.md b/src/SKILL.md index 14ea17c..473d385 100644 --- a/src/SKILL.md +++ b/src/SKILL.md @@ -28,7 +28,8 @@ keeping its formatting syntax. npx -y human-review http://localhost:3000/wiki ``` -3. Wait for feedback. This blocks until they hit Send, or the timeout passes: +3. Wait for feedback. This blocks until they hit **Send**, hit **No change**, + close the review tab, or the timeout passes: ```sh npx -y human-review poll path/to/file.html --timeout 600 @@ -36,21 +37,28 @@ keeping its formatting syntax. Keep this command in the foreground. Do not end your turn while it is waiting. If your shell returns a process or session handle, keep waiting on that handle - until the command exits. If it prints `{"status":"timeout"}`, no feedback has - arrived yet — run the same poll command again to keep waiting. Feedback is - saved even if a poll dies, so nothing is ever lost. + until the command exits. - If it prints `{"status":"closed"}`, the user ended the review from the - browser — stop polling and do not run the poll command again. Unsent - feedback is kept and ships the next time this target is reviewed. + **Stop polling. Do not run poll again when:** + - it prints `{"status":"closed"}` — they hit No change or closed the tab + - they say they are done in chat (kill any still-running poll) -4. Apply what comes back, then wait again. `--ack` clears the batch you just handled: + **Timeout:** if it prints `{"status":"timeout"}`, run `npx -y human-review status ` + first. If status is `closed`, stop. If the user has moved on in chat, stop and + kill the poll. Only re-poll if you are still waiting on this review. + + Never leave a poll running in the background after the review is over. + Closing the browser is a decision: it ends the review. + +4. Apply what comes back, then wait again. `--ack` clears a **feedback** batch: ```sh npx -y human-review poll path/to/file.html --ack --timeout 600 ``` -Repeat 3–4 until the user says they are done. + Do **not** `--ack` a `closed` batch. That review is finished. + +Repeat 3–4 until the user is done (`closed`, or they say so). Not sure whether feedback is already waiting — say, at the start of a new turn? This answers instantly without blocking: @@ -60,6 +68,7 @@ npx -y human-review status path/to/file.html ``` It prints `{"status": "feedback-waiting"}` when a batch is ready for a poll, +`{"status": "closed"}` when they finished with no further changes, plus counts of unsent comments and edits still in the browser. ## What you get @@ -89,6 +98,20 @@ One batch covers every page the user visited, grouped by file or localhost URL. } ``` +A finished review with nothing to apply looks like: + +```json +{ + "status": "closed", + "reason": "no_change", + "next_step": "The user ended this review session. Stop polling — do not run the poll command again." +} +``` + +`reason` is `no_change` (they clicked No change) or `window_closed` (they +closed the tab). Either way: stop. Unsent comments from a closed tab are kept +and ship the next time this target is reviewed. + ## Rules - **`edits` are changes the user already made.** `after` is their exact wording — diff --git a/src/chrome-client.js b/src/chrome-client.js index 4143b8f..c3d6b22 100644 --- a/src/chrome-client.js +++ b/src/chrome-client.js @@ -22,6 +22,7 @@ const state = { save: "idle", savedAt: "", sent: false, + closed: false, orphans: new Set(), pollCommand: "", editsExpanded: false, @@ -303,10 +304,14 @@ function render() { // note-only batches; the button must not stay dead while one is typed. const hasNote = $("note").value.trim().length > 0; const send = $("send"); + const end = $("endReview"); const delivered = state.agent === "working"; const stranded = state.agent === "stranded"; - const busy = delivered || stranded || state.sent; - send.disabled = (total === 0 && !hasNote) || busy; + const finished = state.agent === "closed" || state.closed; + const busy = delivered || stranded || state.sent || finished; + const empty = total === 0 && !hasNote; + send.disabled = empty || busy; + send.hidden = empty && !busy && !delivered && !stranded && !state.sent; send.textContent = delivered ? "Feedback delivered" : stranded @@ -318,13 +323,20 @@ function render() { : hasNote ? "Send note to agent" : "Nothing to send yet"; - if (!send.disabled) { + if (!send.disabled && !send.hidden) { const key = document.createElement("span"); key.className = "key"; key.textContent = "⌘⏎"; send.append(" ", key); } + end.disabled = busy; + end.classList.toggle("primary", empty && !busy); + end.textContent = finished ? "Review ended" : empty ? "No change" : "End review"; + end.title = empty + ? "Looks good — end the review without comments" + : "Stop this review and release the waiting agent"; + // After sending, say what happens next. If nothing is polling, the loop would // otherwise dead-end silently, so hand over the exact command to run. $("agentLine").hidden = !delivered; @@ -701,14 +713,22 @@ $("endReview").addEventListener("click", async () => { const page = state.page; const otherTotal = (state.others || []).reduce((sum, o) => sum + o.count, 0); const unsent = page ? (page.comments || []).length + (page.edits || []).length + otherTotal : 0; - const message = unsent - ? `End this review? ${unsent} unsent ${unsent === 1 ? "item" : "items"} will be kept for next time.` - : "End this review? The waiting agent will be told to stop polling."; - if (!window.confirm(message)) return; + const hasNote = $("note").value.trim().length > 0; + if (unsent || hasNote) { + const message = unsent + ? `End this review? ${unsent} unsent ${unsent === 1 ? "item" : "items"} will be kept for next time.` + : "End this review? The waiting agent will be told to stop polling."; + if (!window.confirm(message)) return; + } // Ship anything still sitting in the SDK's debounce windows first. await flushFrame(); try { - await api(`/api/session/${state.sessionId}/end`, { method: "POST" }); + await api(`/api/session/${state.sessionId}/end`, { + method: "POST", + body: JSON.stringify({ reason: "no_change" }), + }); + state.closed = true; + state.agent = "closed"; showEnded(); } catch (err) { toast(err.message); @@ -766,7 +786,8 @@ document.addEventListener("keydown", (event) => { const meta = event.metaKey || event.ctrlKey; if (meta && event.key === "Enter") { event.preventDefault(); - if (!$("send").disabled) $("send").click(); + if (!$("send").disabled && !$("send").hidden) $("send").click(); + else if (!$("endReview").disabled) $("endReview").click(); return; } // ⌘S is reassurance only: flush pending keystrokes, never a state change. diff --git a/src/chrome.css b/src/chrome.css index 6440d14..9f2166d 100644 --- a/src/chrome.css +++ b/src/chrome.css @@ -281,6 +281,18 @@ body.collapsed .handle { right: 0; } border-radius: 6px; } .end-review:hover { color: var(--danger); background: var(--danger-soft); } +.end-review.primary { + margin-top: 0; + margin-bottom: 8px; + padding: 10px 12px; + border: 1px solid var(--btn-bg); + background: var(--btn-bg); + color: var(--btn-fg); + font-size: 12.5px; + font-weight: 500; +} +.end-review.primary:hover { filter: brightness(1.08); color: var(--btn-fg); background: var(--btn-bg); } +.end-review:disabled { color: var(--faint); cursor: default; background: none; } .ended { position: fixed; diff --git a/src/chrome.html b/src/chrome.html index f651a70..3dba6c7 100644 --- a/src/chrome.html +++ b/src/chrome.html @@ -42,7 +42,7 @@
-
Select text or click an element to comment on it
+
Select text or click a block to comment. If this is fine, hit No change — closing the tab also ends the review.