Progress: the reply's line is the 3.4.0 progress object, {{text}} · {{elapsed}}
ci / check (push) Failing after 3s
ci / start (push) Failing after 2s

ui/js/progress.js draws harness spec 3.4.0's `progress` (progress.md) with the
spec's texts as the page's sentences: the thirteen phases, their variants and
field words, the line with its clock in whole seconds (5s, 1m 05s, 1h 02m),
none for stopped and failed, retrying counted down. A live reply takes
`progress` from any stream frame that carries it and ticks once a second; with
no progress (an older server, or null) it draws the status text as before.

The sentences are the server's to translate: gen-i18n.py takes them from its
catalogues once they are there (SERVER_PENDING), English until then, without
failing.

tests/ui: every case of the spec's conformance/progress.json (copied, 3.4.0),
and a live stream's progress, clock, fallback and stopped line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-11-10 09:10:20 +00:00
1 parent 3ef0bbaad3
commit 3baae016e1
7 files changed
+607 -2

No files matched your search

+30 -1
View File
@@ -18,6 +18,7 @@ import { readyIds, sending, sent, discard } from "./attach.js"
import { watchActivity, activityChanged, planList, activityButton } from "./activity.js"
import { agentBlock, agentReady, agentState, onAgentChange } from "./agent.js"
import { folderName } from "./list.js"
import { isProgress, progressLine, progressTicks } from "./progress.js"
import { aimTerminal, terminalButton, terminalOpen, toggleTerminal } from "./terminal.js"
const MODES = ["manual", "edit", "auto", "plan"]
@@ -351,6 +352,24 @@ class Live {
this.el = h("article", { class: "msg msg--assistant is-live", dataset: { id: messageId } }, this.steps, this.thinking, this.tail, this.jobs, this.status)
this.done = false
this.handle = null
this.progress = null // the stream's `progress` (spec 3.4.0), drawn in place of `status` when it comes
this.said = "" // the last `status` text, for a server that sends no progress
this.clock = 0
}
/** The status line: the progress object's line where the server sends one, else its text. */
drawStatus() {
const p = this.progress
this.status.textContent = isProgress(p) ? progressLine(p) : this.said || t("Writing…")
this.status.dataset.phase = isProgress(p) ? p.phase : ""
const ticks = progressTicks(p)
if (ticks && !this.clock) this.clock = setInterval(() => (this.done ? this.stopClock() : this.drawStatus()), 1000)
else if (!ticks) this.stopClock()
}
stopClock() {
clearInterval(this.clock)
this.clock = 0
}
async start() {
@@ -366,11 +385,18 @@ class Live {
stop() {
this.done = true
this.stopClock()
this.handle?.close()
}
frame(event, data) {
if (this.done) return
// Any frame that carries `progress` (the status frame does, from server 2.0.0-beta.8 on):
// the object, or null for nothing running -- then the status text again.
if (data && typeof data === "object" && Object.prototype.hasOwnProperty.call(data, "progress")) {
this.progress = isProgress(data.progress) ? data.progress : null
if (event !== "status") this.drawStatus()
}
switch (event) {
case "steps":
renderSteps(this.steps, data.steps || [], this.chat)
@@ -380,7 +406,8 @@ class Live {
this.tailed(data)
break
case "status":
this.status.textContent = data.text || t("Writing…")
this.said = data.text || ""
this.drawStatus()
break
case "ask":
renderCards(this.chat, data.cards || [])
@@ -399,6 +426,7 @@ class Live {
break
case "done":
this.done = true
this.stopClock()
this.el.replaceWith(assistantMessage(data.message, this.chat))
renderCards(this.chat, [])
if (data.chat?.title) setTitle(data.chat.title)
@@ -424,6 +452,7 @@ class Live {
end(err) {
if (this.done) return
this.done = true
this.stopClock()
if (open?.live === this) open.live = null
setRunning(false)
if (err) {