retake-dev 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +91 -0
- package/README.md +198 -0
- package/bin/retake.js +319 -0
- package/package.json +87 -0
- package/src/code-versions.js +357 -0
- package/src/core.js +265 -0
- package/src/plugin.js +137 -0
- package/src/runtime/00-core.js +308 -0
- package/src/runtime/10-animations.js +362 -0
- package/src/runtime/20-media.js +157 -0
- package/src/runtime/30-recorder.js +315 -0
- package/src/runtime/32-state.js +315 -0
- package/src/runtime/35-network.js +789 -0
- package/src/runtime/36-scripts.js +129 -0
- package/src/runtime/37-next.js +150 -0
- package/src/runtime/38-observers.js +252 -0
- package/src/runtime/40-input.js +795 -0
- package/src/runtime/45-hover.js +78 -0
- package/src/runtime/50-engine.js +341 -0
- package/src/runtime/60-preview.js +437 -0
- package/src/runtime/65-timeline.js +289 -0
- package/src/runtime/66-activity.js +114 -0
- package/src/runtime/67-csssource.js +226 -0
- package/src/runtime/70-boot.js +460 -0
- package/src/server/api.js +285 -0
- package/src/server/child.js +174 -0
- package/src/server/detect.js +167 -0
- package/src/server/front.js +647 -0
- package/src/server/mcp.js +332 -0
- package/src/shell/00-state.js +85 -0
- package/src/shell/05-api.js +136 -0
- package/src/shell/10-dock.js +754 -0
- package/src/shell/12-checkpoint.js +121 -0
- package/src/shell/15-session.js +291 -0
- package/src/shell/20-timeline.js +734 -0
- package/src/shell/25-input.js +537 -0
- package/src/shell/30-notes.js +870 -0
- package/src/shell/40-code.js +80 -0
- package/src/shell/90-handle.js +15 -0
- package/src/shell/shell.css +295 -0
- package/src/shell/shell.html +59 -0
- package/types/client.d.ts +73 -0
- package/types/index.d.ts +111 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Checkpoints. Going back in time rebuilds the moment from the start of its
|
|
2
|
+
// page, which on a heavy app takes a while (the app renders every recorded
|
|
3
|
+
// frame again). So after a slow rewind to t, while you look around in the
|
|
4
|
+
// past, a hidden paused frame is built (in idle time) at a moment a little
|
|
5
|
+
// before t. The next rewind to anywhere at or after it (same timeline, same
|
|
6
|
+
// code) hands that frame the recording (PT.adopt) and it only seeks forward
|
|
7
|
+
// from there. Using it spends it; another is built once things are quiet again.
|
|
8
|
+
// Only one frame runs the app at a time: a checkpoint still being built stops
|
|
9
|
+
// when anything else needs to run (quietCheckpoint), and none is made for an
|
|
10
|
+
// app that keeps state in IndexedDB (another frame's replay would change it
|
|
11
|
+
// under the visible one, see storageOk in the runtime).
|
|
12
|
+
|
|
13
|
+
const CP_BACK = 1500 // how far behind the last rewind the checkpoint sits
|
|
14
|
+
const CP_QUIET = 300 // ms of no rebuilding before one is started
|
|
15
|
+
D.cpMinMs = 150 // only for apps whose replays (of the last rebuild) take at least this long
|
|
16
|
+
|
|
17
|
+
let lastBuild = null // { target, branchId, at }
|
|
18
|
+
|
|
19
|
+
// A rebuild just finished: remember how long its replay took and where it went.
|
|
20
|
+
// (An app whose replays are slow stays "heavy": a rewind a checkpoint made
|
|
21
|
+
// quick still wants the next checkpoint.)
|
|
22
|
+
function noteBuilt(b) {
|
|
23
|
+
if (!b || b.target == null || b.startedAt == null) return
|
|
24
|
+
const ms = performance.now() - b.startedAt
|
|
25
|
+
let seekMs = null
|
|
26
|
+
try {
|
|
27
|
+
seekMs = b.pt.debug().seekMs
|
|
28
|
+
} catch {}
|
|
29
|
+
if (b.via !== "checkpoint" && (seekMs != null ? seekMs : ms) >= D.cpMinMs) D.heavy = true
|
|
30
|
+
lastBuild = { target: b.target, branchId: b.branchId, at: performance.now() }
|
|
31
|
+
D.lastRebuild = { ms: Math.round(ms), seekMs, via: b.via || "replay" }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function dropCheckpoint() {
|
|
35
|
+
if (!D.cp) return
|
|
36
|
+
D.cp.frame.remove()
|
|
37
|
+
D.cp = null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The frame on show is about to run its app (play, an in-place seek): a
|
|
41
|
+
// checkpoint still being built stops. A ready one is paused and stays.
|
|
42
|
+
function quietCheckpoint() {
|
|
43
|
+
if (D.cp && !checkpointReady(D.cp)) dropCheckpoint()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Each frame: drop a checkpoint that no longer fits, start one when it's
|
|
47
|
+
// worth it and quiet.
|
|
48
|
+
function tendCheckpoint() {
|
|
49
|
+
const cp = D.cp
|
|
50
|
+
const active = activeBranch()
|
|
51
|
+
if (cp && (cp.branchId !== D.activeId || cp.version !== (active && active.version))) dropCheckpoint()
|
|
52
|
+
// The frame on show runs its app again some other way (⌥P in the app, an
|
|
53
|
+
// API seek): a checkpoint still being built stops.
|
|
54
|
+
if (D.cp && D.last && (D.last.playing || D.last.seeking) && !checkpointReady(D.cp)) dropCheckpoint()
|
|
55
|
+
if (D.cp || D.building || !lastBuild || !D.PT || D.switching || D.dragT != null || D.keyT != null) return
|
|
56
|
+
if (!D.heavy || lastBuild.branchId !== D.activeId) return
|
|
57
|
+
if (performance.now() - lastBuild.at < CP_QUIET) return
|
|
58
|
+
const s = D.last
|
|
59
|
+
if (!s || !s.started || s.seeking || s.previewing || s.playing || s.idb || isInteractive(s)) return
|
|
60
|
+
let rec
|
|
61
|
+
let json
|
|
62
|
+
try {
|
|
63
|
+
rec = D.PT.history()
|
|
64
|
+
json = JSON.stringify(rec)
|
|
65
|
+
} catch {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
// A little before the last rewind, on the page it was on.
|
|
69
|
+
let page = 0
|
|
70
|
+
for (const sg of rec.segments || []) if (sg.t <= lastBuild.target) page = sg.t
|
|
71
|
+
const at = Math.max(s.start, page, lastBuild.target - CP_BACK)
|
|
72
|
+
const url = new URL(urlAt(rec, at), location.href)
|
|
73
|
+
let doc = rec.doc
|
|
74
|
+
for (const sg of rec.segments || []) if (sg.t <= at) doc = sg.doc
|
|
75
|
+
askStoredDoc(doc, url.href)
|
|
76
|
+
const id = ++D.buildSeq
|
|
77
|
+
const frame = makeFrame(url.pathname + url.search + url.hash, { rec: json, target: at, play: false, rate: 1, buildId: id })
|
|
78
|
+
frame.className = "checkpoint"
|
|
79
|
+
frame.setAttribute("aria-hidden", "true")
|
|
80
|
+
frame.tabIndex = -1
|
|
81
|
+
const vp = okViewport(rec.viewport) || recordedViewport(rec)
|
|
82
|
+
if (vp) {
|
|
83
|
+
frame.style.width = vp.w + "px"
|
|
84
|
+
frame.style.height = vp.h + "px"
|
|
85
|
+
}
|
|
86
|
+
D.cp = { id, frame, pt: null, at, branchId: D.activeId, version: active && active.version }
|
|
87
|
+
lastBuild = null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const checkpointReady = (cp) => {
|
|
91
|
+
try {
|
|
92
|
+
const s = cp.pt && cp.pt.state()
|
|
93
|
+
return !!s && s.booted && s.target == null && !s.seeking
|
|
94
|
+
} catch {
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Called for every rewind: true if the checkpoint took it (it's the build in
|
|
100
|
+
// flight now, seeking forward from where it was parked).
|
|
101
|
+
function useCheckpoint(payload) {
|
|
102
|
+
const cp = D.cp
|
|
103
|
+
if (!cp || cp.branchId !== D.activeId || payload.target < cp.at || !checkpointReady(cp) || typeof cp.pt.adopt !== "function") return false
|
|
104
|
+
let ok = false
|
|
105
|
+
try {
|
|
106
|
+
ok = cp.pt.adopt(String(payload.rec), Number(payload.target), false)
|
|
107
|
+
} catch {}
|
|
108
|
+
D.cp = null
|
|
109
|
+
if (!ok) {
|
|
110
|
+
cp.frame.remove()
|
|
111
|
+
return false
|
|
112
|
+
}
|
|
113
|
+
cp.frame.className = "building"
|
|
114
|
+
cp.frame.removeAttribute("tabindex")
|
|
115
|
+
try {
|
|
116
|
+
if (typeof cp.pt.setBackground === "function") cp.pt.setBackground(D.dragT != null || D.keyT != null)
|
|
117
|
+
} catch {}
|
|
118
|
+
window.__retakeShell.rebuilding = true
|
|
119
|
+
D.building = { id: cp.id, frame: cp.frame, pt: cp.pt, stash: null, branchId: D.activeId, target: payload.target, play: false, fork: false, visible: true, startedAt: performance.now(), via: "checkpoint", restarts: 0 }
|
|
120
|
+
return true
|
|
121
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// Keeping the session. Timelines, notes and bookmarks go to the dev server
|
|
2
|
+
// (/__retake/session, kept on disk under <project>/.retake/) so a reload or a
|
|
3
|
+
// restart doesn't lose them; each timeline's recording goes to
|
|
4
|
+
// /__retake/recording/:id. Agents reply to notes over /__retake/events.
|
|
5
|
+
// When the server has no session API (it answers 404), everything stays in
|
|
6
|
+
// memory for this page.
|
|
7
|
+
const TOKEN = window.__RETAKE_TOKEN || ""
|
|
8
|
+
const net = {
|
|
9
|
+
on: true, // false: in-memory only
|
|
10
|
+
restoring: true, // nothing is saved until what's on disk has been read
|
|
11
|
+
savedSession: null, // the last session body the server has
|
|
12
|
+
savedRec: new Map(), // branch id → the recording string the server has
|
|
13
|
+
activeSig: "", // what the active recording looked like when last saved
|
|
14
|
+
activeSavedAt: 0,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function api(method, path, body) {
|
|
18
|
+
const headers = {}
|
|
19
|
+
if (method !== "GET") headers["x-retake-token"] = TOKEN
|
|
20
|
+
if (body !== undefined) headers["content-type"] = "application/json"
|
|
21
|
+
const res = await fetch("/__retake/" + path, {
|
|
22
|
+
method,
|
|
23
|
+
headers,
|
|
24
|
+
body: body === undefined ? undefined : typeof body === "string" ? body : JSON.stringify(body),
|
|
25
|
+
})
|
|
26
|
+
if (res.status === 404) return { missing: true }
|
|
27
|
+
if (!res.ok) throw new Error(`${method} /__retake/${path}: ${res.status}`)
|
|
28
|
+
const text = await res.text()
|
|
29
|
+
return { value: text && (res.headers.get("content-type") || "").includes("json") ? JSON.parse(text) : text }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The session as the server stores it (see CONTRACT.md).
|
|
33
|
+
function sessionBody() {
|
|
34
|
+
return {
|
|
35
|
+
branches: D.branches.map((b) => ({ id: b.id, parentId: b.parentId, forkAt: b.forkAt, name: b.name, codeVersion: b.version || null, end: b.end })),
|
|
36
|
+
activeId: D.activeId,
|
|
37
|
+
markers: D.markers,
|
|
38
|
+
notes: D.notes.map(noteForServer),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function applySession(s) {
|
|
43
|
+
D.branches = s.branches.map((b) => ({ ...b, version: b.codeVersion || undefined, json: null, end: b.end ?? b.forkAt, born: -1e9 }))
|
|
44
|
+
D.branchSeq = Math.max(0, ...D.branches.map((b) => b.id))
|
|
45
|
+
D.activeId = D.branches.some((b) => b.id === s.activeId) ? s.activeId : D.branches[0].id
|
|
46
|
+
D.markers = Array.isArray(s.markers) ? s.markers : []
|
|
47
|
+
D.markerSeq = Math.max(0, ...D.markers.map((m) => Number(m.id) || 0))
|
|
48
|
+
D.notes = Array.isArray(s.notes) ? s.notes.map(noteFromServer) : []
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// A timeline's recording: in memory if we have it, else from the server.
|
|
52
|
+
async function recordingOf(b) {
|
|
53
|
+
if (b.json) return b.json
|
|
54
|
+
if (!net.on) return null
|
|
55
|
+
try {
|
|
56
|
+
const r = await api("GET", `recording/${b.id}`)
|
|
57
|
+
if (r.missing || !r.value) return null
|
|
58
|
+
b.json = typeof r.value === "string" ? r.value : JSON.stringify(r.value)
|
|
59
|
+
net.savedRec.set(b.id, b.json)
|
|
60
|
+
return b.json
|
|
61
|
+
} catch {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The page the user asked for, without the frame's own parameter.
|
|
67
|
+
const askedFor = location.pathname + location.search + location.hash
|
|
68
|
+
const FEATURES = (window.__retakeConfig && window.__retakeConfig.features) || []
|
|
69
|
+
|
|
70
|
+
// On load: read what was saved, then open the app at the page the user asked
|
|
71
|
+
// for, live. The saved timelines stay; the active one carries on from its end
|
|
72
|
+
// at this page (a runtime with the "continue" feature marks it there, and a
|
|
73
|
+
// rewind before that point brings the old page back). A runtime without it
|
|
74
|
+
// gets this visit as a timeline of its own, so nothing saved is overwritten.
|
|
75
|
+
// It's never a rebuild, and never waits long: after 1.5s the app opens fresh.
|
|
76
|
+
async function restore() {
|
|
77
|
+
let known = false // the saved session has been read (or there's none)
|
|
78
|
+
const fallback = setTimeout(() => !D.frame && openFirstFrame(null, null), 1500)
|
|
79
|
+
const ownTimeline = () => {
|
|
80
|
+
const b = newBranch(0, null)
|
|
81
|
+
b.name = `→ ${askedFor.split("?")[0] || "/"}`
|
|
82
|
+
D.activeId = b.id
|
|
83
|
+
return b.id
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const r = await api("GET", "session")
|
|
87
|
+
if (r.missing) {
|
|
88
|
+
net.on = false
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
const s = r.value
|
|
92
|
+
if (!s || !Array.isArray(s.branches) || !s.branches.length) return
|
|
93
|
+
applySession(s)
|
|
94
|
+
net.savedSession = JSON.stringify(sessionBody())
|
|
95
|
+
known = true
|
|
96
|
+
const json = await recordingOf(activeBranch())
|
|
97
|
+
if (D.frame) {
|
|
98
|
+
// Opened fresh already (the session was slow): that visit is its own.
|
|
99
|
+
D.frameBranch = ownTimeline()
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
if (json && FEATURES.includes("continue")) {
|
|
103
|
+
let end = activeBranch().end
|
|
104
|
+
try {
|
|
105
|
+
end = JSON.parse(json).end
|
|
106
|
+
} catch {}
|
|
107
|
+
openFirstFrame({ rec: json, target: end, play: true, continue: true, url: askedFor }, D.activeId)
|
|
108
|
+
} else if (json) openFirstFrame(null, ownTimeline())
|
|
109
|
+
else openFirstFrame(null, D.activeId)
|
|
110
|
+
} catch (err) {
|
|
111
|
+
// Don't overwrite a session we couldn't read.
|
|
112
|
+
console.warn("[retake] couldn't restore the session; keeping this one in memory", err)
|
|
113
|
+
net.on = false
|
|
114
|
+
} finally {
|
|
115
|
+
clearTimeout(fallback)
|
|
116
|
+
if (!known && !D.frame) openFirstFrame(null, D.activeId)
|
|
117
|
+
else if (!known && D.frameBranch == null) D.frameBranch = D.activeId
|
|
118
|
+
net.restoring = false
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The active timeline's recording is saved when it's still (paused), and every
|
|
123
|
+
// few seconds while it's live.
|
|
124
|
+
function activeSig(s) {
|
|
125
|
+
const h = D.PT.history()
|
|
126
|
+
return `${D.activeId}:${h.end}:${h.events.length}:${h.frames.length}`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let saving = false
|
|
130
|
+
async function persist() {
|
|
131
|
+
if (!net.on || net.restoring || net.hold || saving || D.switching) return
|
|
132
|
+
saving = true
|
|
133
|
+
try {
|
|
134
|
+
const body = JSON.stringify(sessionBody())
|
|
135
|
+
if (body !== net.savedSession) {
|
|
136
|
+
await api("PUT", "session", body)
|
|
137
|
+
net.savedSession = body
|
|
138
|
+
}
|
|
139
|
+
for (const b of D.branches) {
|
|
140
|
+
if (b.id === D.activeId || !b.json || net.savedRec.get(b.id) === b.json) continue
|
|
141
|
+
await api("PUT", `recording/${b.id}`, b.json)
|
|
142
|
+
net.savedRec.set(b.id, b.json)
|
|
143
|
+
}
|
|
144
|
+
const s = state()
|
|
145
|
+
if (D.PT && s && s.started && !D.building && D.frameBranch === D.activeId) {
|
|
146
|
+
const sig = activeSig(s)
|
|
147
|
+
const due = !s.recording || performance.now() - net.activeSavedAt > 5000
|
|
148
|
+
if (sig !== net.activeSig && due) {
|
|
149
|
+
const json = JSON.stringify(D.PT.history())
|
|
150
|
+
await api("PUT", `recording/${D.activeId}`, json)
|
|
151
|
+
net.activeSig = sig
|
|
152
|
+
net.activeSavedAt = performance.now()
|
|
153
|
+
net.savedRec.set(D.activeId, json)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.warn("[retake] saving the session failed; will retry", err)
|
|
158
|
+
} finally {
|
|
159
|
+
saving = false
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Debounced: a change is saved once things have been still for 300ms.
|
|
164
|
+
let lastSeen = ""
|
|
165
|
+
let stillSince = 0
|
|
166
|
+
setInterval(() => {
|
|
167
|
+
if (!net.on || net.restoring || net.hold) return
|
|
168
|
+
let sig = ""
|
|
169
|
+
try {
|
|
170
|
+
sig = JSON.stringify(sessionBody()) + D.branches.map((b) => (b.json ? b.json.length : 0)).join(",")
|
|
171
|
+
if (D.PT && D.last && D.last.started) sig += activeSig(D.last)
|
|
172
|
+
} catch {
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
const now = performance.now()
|
|
176
|
+
if (sig !== lastSeen) {
|
|
177
|
+
lastSeen = sig
|
|
178
|
+
stillSince = now
|
|
179
|
+
// A live recording never stands still; save it on its own clock.
|
|
180
|
+
if (!(D.last && D.last.recording)) return
|
|
181
|
+
}
|
|
182
|
+
if (now - stillSince >= 300 || (D.last && D.last.recording)) persist()
|
|
183
|
+
}, 100)
|
|
184
|
+
|
|
185
|
+
// Agent replies and other outside changes arrive as server events.
|
|
186
|
+
function listen() {
|
|
187
|
+
if (!window.EventSource || !net.on) return
|
|
188
|
+
const es = new EventSource("/__retake/events")
|
|
189
|
+
const read = (e) => {
|
|
190
|
+
try {
|
|
191
|
+
return JSON.parse(e.data)
|
|
192
|
+
} catch {
|
|
193
|
+
return null
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
es.addEventListener("note-updated", (e) => {
|
|
197
|
+
const d = read(e)
|
|
198
|
+
const n = d && (d.note || d)
|
|
199
|
+
if (n && n.id != null) mergeNote(n)
|
|
200
|
+
})
|
|
201
|
+
es.addEventListener("active-changed", (e) => {
|
|
202
|
+
const d = read(e)
|
|
203
|
+
const id = d && Number(d.activeId)
|
|
204
|
+
if (id && id !== D.activeId && branchById(id)) switchTo(id, D.last ? D.last.now : 0)
|
|
205
|
+
})
|
|
206
|
+
es.addEventListener("code-version", (e) => {
|
|
207
|
+
const d = read(e)
|
|
208
|
+
if (d && d.version) D.codeVersionSeen = d.version
|
|
209
|
+
})
|
|
210
|
+
// A server without events answers 404 and the stream closes for good.
|
|
211
|
+
es.onerror = () => es.readyState === EventSource.CLOSED && es.close()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Start fresh: one empty timeline, no notes, a new recording, straight away.
|
|
215
|
+
// For five seconds it can be undone; the server isn't told until then.
|
|
216
|
+
const UNDO_MS = 5000
|
|
217
|
+
let undo = null
|
|
218
|
+
const toast = $("#wb-toast")
|
|
219
|
+
|
|
220
|
+
function startFresh() {
|
|
221
|
+
if (undo) commitFresh()
|
|
222
|
+
const snap = {
|
|
223
|
+
branches: D.branches.map((b) => ({ ...b })),
|
|
224
|
+
activeId: D.activeId,
|
|
225
|
+
branchSeq: D.branchSeq,
|
|
226
|
+
notes: D.notes,
|
|
227
|
+
markers: D.markers,
|
|
228
|
+
markerSeq: D.markerSeq,
|
|
229
|
+
frame: D.frame, // the frame showing when it was cleared
|
|
230
|
+
}
|
|
231
|
+
const cur = snap.branches.find((b) => b.id === snap.activeId)
|
|
232
|
+
try {
|
|
233
|
+
if (cur && D.PT) {
|
|
234
|
+
cur.json = JSON.stringify(D.PT.history())
|
|
235
|
+
cur.end = D.PT.state().end
|
|
236
|
+
}
|
|
237
|
+
} catch {}
|
|
238
|
+
net.hold = true
|
|
239
|
+
D.notes = []
|
|
240
|
+
D.markers = []
|
|
241
|
+
D.markerSeq = 0
|
|
242
|
+
closeCard()
|
|
243
|
+
resetBranches()
|
|
244
|
+
invalidateGutter()
|
|
245
|
+
freshFrame()
|
|
246
|
+
undo = { snap, timer: setTimeout(commitFresh, UNDO_MS) }
|
|
247
|
+
toast.hidden = false
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// The five seconds are up: clear the session on the server too.
|
|
251
|
+
async function commitFresh() {
|
|
252
|
+
if (!undo) return
|
|
253
|
+
clearTimeout(undo.timer)
|
|
254
|
+
undo = null
|
|
255
|
+
toast.hidden = true
|
|
256
|
+
net.savedRec.clear()
|
|
257
|
+
net.activeSig = ""
|
|
258
|
+
net.hold = false
|
|
259
|
+
if (net.on) {
|
|
260
|
+
try {
|
|
261
|
+
const body = JSON.stringify(sessionBody())
|
|
262
|
+
await api("PUT", "session", body)
|
|
263
|
+
net.savedSession = body
|
|
264
|
+
} catch {}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Undo: everything back as it was, the moment rebuilt where it was.
|
|
269
|
+
function undoFresh() {
|
|
270
|
+
if (!undo) return
|
|
271
|
+
clearTimeout(undo.timer)
|
|
272
|
+
const { snap } = undo
|
|
273
|
+
undo = null
|
|
274
|
+
toast.hidden = true
|
|
275
|
+
D.branches = snap.branches
|
|
276
|
+
D.activeId = snap.activeId
|
|
277
|
+
D.branchSeq = snap.branchSeq
|
|
278
|
+
D.notes = snap.notes
|
|
279
|
+
D.markers = snap.markers
|
|
280
|
+
D.markerSeq = snap.markerSeq
|
|
281
|
+
invalidateGutter()
|
|
282
|
+
const cur = activeBranch()
|
|
283
|
+
// Still showing the old frame (the fresh one hadn't swapped in yet)? Then
|
|
284
|
+
// there's nothing to rebuild: just drop the fresh one.
|
|
285
|
+
if (D.building) cancelBuild()
|
|
286
|
+
if (D.frame === snap.frame) D.frameBranch = cur.id
|
|
287
|
+
else if (cur && cur.json && D.PT) D.PT.load(cur.json, cur.end)
|
|
288
|
+
net.hold = false
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
restore().then(listen)
|