slidemind 0.3.20 → 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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +591 -15
- package/README.md +202 -7
- package/bin/slidemind.mjs +50 -2
- package/package.json +2 -2
- package/skills/create-slides/SKILL.md +484 -886
- package/skills/search/SKILL.md +46 -23
- package/src/bridge/build-and-push.mjs +548 -73
- package/src/bridge/bundle.mjs +100 -31
- package/src/bridge/capture.mjs +14 -17
- package/src/bridge/client/brands.mjs +157 -18
- package/src/bridge/client/engine-routing.mjs +48 -13
- package/src/bridge/client/engine-search.mjs +24 -1
- package/src/bridge/client/render.mjs +27 -1
- package/src/bridge/client/transport.mjs +499 -23
- package/src/bridge/client.mjs +10 -0
- package/src/bridge/config.mjs +67 -6
- package/src/bridge/engine-download.mjs +0 -10
- package/src/bridge/graph-auth.mjs +55 -3
- package/src/bridge/image-intent-envelope.mjs +239 -0
- package/src/bridge/package-preview.mjs +67 -0
- package/src/bridge/sharepoint.mjs +914 -0
- package/src/builders/asset-fetch.mjs +203 -0
- package/src/builders/block-dividers.mjs +83 -0
- package/src/builders/build.mjs +171 -87
- package/src/builders/card-height.mjs +294 -0
- package/src/builders/chart-hole.mjs +183 -0
- package/src/builders/chart-jsx.mjs +410 -0
- package/src/builders/chart-labels.mjs +326 -0
- package/src/builders/concept-geometry.mjs +370 -0
- package/src/builders/concept-intent.mjs +200 -0
- package/src/builders/concept-layout.mjs +79 -0
- package/src/builders/concept-lines.mjs +509 -0
- package/src/builders/concept-lists.mjs +270 -0
- package/src/builders/concept-marks.mjs +84 -0
- package/src/builders/concept-relationships.mjs +12 -0
- package/src/builders/concept-spec.mjs +645 -0
- package/src/builders/concept-text.mjs +140 -0
- package/src/builders/concept-waves.mjs +57 -0
- package/src/builders/copy-fit.mjs +139 -0
- package/src/builders/design-tokens.mjs +347 -0
- package/src/builders/draft-check.mjs +552 -340
- package/src/builders/draft-lines.mjs +369 -0
- package/src/builders/edit-route.mjs +29 -0
- package/src/builders/figure-sketch.mjs +375 -0
- package/src/builders/fold-sets.mjs +639 -0
- package/src/builders/gate-server.mjs +9 -4
- package/src/builders/gate-session.js +273 -12
- package/src/builders/gate-shell.js +1494 -0
- package/src/builders/image-intent-check.mjs +308 -0
- package/src/builders/image-use-reconcile.mjs +107 -0
- package/src/builders/kit-helpers.mjs +121 -0
- package/src/builders/layout-names.mjs +32 -0
- package/src/builders/legacy-reference-uses.mjs +179 -0
- package/src/builders/line-fit.mjs +224 -0
- package/src/builders/line-native.mjs +895 -0
- package/src/builders/line-relations.mjs +230 -0
- package/src/builders/lint-fix.mjs +117 -9
- package/src/builders/lint.mjs +25 -20
- package/src/builders/list-jsx.mjs +88 -0
- package/src/builders/placement.mjs +133 -0
- package/src/builders/plan-checks.mjs +135 -0
- package/src/builders/plan-comments.mjs +19 -0
- package/src/builders/plan-handlers.mjs +95 -0
- package/src/builders/plan-markdown-edit.mjs +3 -3
- package/src/builders/plan-overlay.js +1530 -1608
- package/src/builders/plan.mjs +689 -186
- package/src/builders/preset-definitions.json +1 -0
- package/src/builders/preset-eval.mjs +244 -0
- package/src/builders/preview.html +51 -120
- package/src/builders/red-thread-backbone.mjs +135 -36
- package/src/builders/repair.mjs +205 -0
- package/src/builders/repeat-groups.mjs +540 -0
- package/src/builders/resolve-skill.mjs +5 -4
- package/src/builders/review-overlay.js +296 -293
- package/src/builders/review.mjs +305 -84
- package/src/builders/rule-shift.mjs +96 -0
- package/src/builders/seat.mjs +354 -0
- package/src/builders/settle-driver.mjs +236 -0
- package/src/builders/settle.mjs +1448 -0
- package/src/builders/slide-prep.mjs +67 -0
- package/src/builders/spec-assets.mjs +236 -0
- package/src/builders/spec-match.mjs +56 -0
- package/src/builders/stack.mjs +413 -0
- package/src/builders/storyboard-markdown.mjs +70 -27
- package/src/builders/storyboard-schema.mjs +54 -114
- package/src/builders/text-blocks.mjs +212 -0
- package/src/builders/text-card-fit.mjs +552 -0
- package/src/builders/text-stack.mjs +276 -0
- package/src/builders/transcribe.mjs +1069 -0
- package/src/cli/assets.mjs +164 -0
- package/src/cli/common.mjs +173 -12
- package/src/cli/concept-spec-support.mjs +76 -0
- package/src/cli/concepts.mjs +1002 -103
- package/src/cli/deck.mjs +401 -65
- package/src/cli/diagnose.mjs +4 -2
- package/src/cli/engine-helpers.mjs +17 -10
- package/src/cli/engine-results.mjs +76 -3
- package/src/cli/help.mjs +232 -28
- package/src/cli/install.mjs +9 -0
- package/src/cli/maintenance.mjs +104 -0
- package/src/cli/read.mjs +2 -1
- package/src/cli/ready.mjs +6 -3
- package/src/cli/record.mjs +522 -0
- package/src/cli/remote.mjs +818 -0
- package/src/cli/revision.mjs +686 -0
- package/src/cli/search.mjs +30 -11
- package/src/cli/session.mjs +56 -13
- package/src/cli/setup.mjs +46 -2
- package/src/cli/status.mjs +142 -33
- package/src/cli/tasks.mjs +246 -0
- package/src/cli/transcribe.mjs +854 -0
- package/src/cli/workspace-ui.mjs +260 -0
- package/src/cli/workspace.mjs +212 -34
- package/src/install-channel.mjs +64 -7
- package/src/lib/adopt.mjs +346 -25
- package/src/lib/concept-spec-binding.mjs +96 -0
- package/src/lib/conversions.mjs +70 -6
- package/src/lib/convert-folded.mjs +107 -0
- package/src/lib/deck-paths.mjs +418 -0
- package/src/lib/durable-ids.mjs +47 -0
- package/src/lib/edit-operations.mjs +316 -0
- package/src/lib/edit-state.mjs +1244 -0
- package/src/lib/edit-transitions.mjs +120 -0
- package/src/lib/extensions.mjs +501 -0
- package/src/lib/image-assets.mjs +465 -0
- package/src/lib/image-derivatives.mjs +234 -0
- package/src/lib/image-intent-fingerprint.mjs +70 -0
- package/src/lib/image-intent-tags.mjs +176 -0
- package/src/lib/image-intent.mjs +802 -0
- package/src/lib/image-use-overrides.mjs +279 -0
- package/src/lib/inline-components.mjs +613 -0
- package/src/lib/lint-converted.mjs +68 -4
- package/src/lib/merge-json.mjs +163 -0
- package/src/lib/overwrite-guard.mjs +3 -3
- package/src/lib/presentation-context.mjs +531 -0
- package/src/lib/preservation-tags.mjs +157 -0
- package/src/lib/preservation-verdict.mjs +1342 -0
- package/src/lib/publish.mjs +1557 -0
- package/src/lib/rebase.mjs +945 -0
- package/src/lib/reconcile-ack.mjs +4 -6
- package/src/lib/reconcile.mjs +62 -24
- package/src/lib/record-client.mjs +175 -0
- package/src/lib/record-state.mjs +151 -0
- package/src/lib/remote.mjs +661 -0
- package/src/lib/revision.mjs +1210 -0
- package/src/lib/slide-guids.mjs +4 -6
- package/src/lib/slide-preservation.mjs +423 -0
- package/src/lib/slide-selection.mjs +9 -0
- package/src/lib/workspace-transaction.mjs +117 -0
- package/src/pptx/content-digest.mjs +334 -0
- package/src/pptx/deck-assemble.mjs +1278 -0
- package/src/pptx/fill/engine.mjs +2 -3
- package/src/pptx/fill/freeform.mjs +6 -3
- package/src/pptx/ooxml.mjs +20 -0
- package/src/pptx/package-evidence.mjs +383 -0
- package/src/pptx/tags.mjs +163 -11
- package/src/update-check.mjs +22 -4
- package/src/workspace/adapters.mjs +222 -0
- package/src/workspace/auth.mjs +256 -0
- package/src/workspace/autostart.mjs +204 -0
- package/src/workspace/daemon.mjs +360 -0
- package/src/workspace/discovery.mjs +180 -0
- package/src/workspace/edit.mjs +167 -0
- package/src/workspace/events.mjs +52 -0
- package/src/workspace/gate-client.mjs +290 -0
- package/src/workspace/handback.mjs +73 -0
- package/src/workspace/lifecycle.mjs +635 -0
- package/src/workspace/operations.mjs +200 -0
- package/src/workspace/pick-folder.mjs +152 -0
- package/src/workspace/registry.mjs +359 -0
- package/src/workspace/remote-links.mjs +397 -0
- package/src/workspace/remote-status.mjs +359 -0
- package/src/workspace/routes.mjs +1331 -0
- package/src/workspace/server.mjs +159 -0
- package/src/workspace/sync.mjs +1119 -0
- package/src/workspace/task-actions.mjs +215 -0
- package/src/workspace/tasks.mjs +507 -0
- package/src/workspace/ui/app.js +1886 -0
- package/src/workspace/ui/index.html +141 -0
- package/src/workspace/ui/plan-bridge.js +120 -0
- package/src/workspace/ui/sharepoint.svg +59 -0
- package/src/workspace/ui/styles.css +423 -0
- package/src/builders/visual-families.mjs +0 -46
|
@@ -68,6 +68,208 @@ const WIRE = {
|
|
|
68
68
|
// is tiny, but a debounce still keeps typing off the storage path entirely.
|
|
69
69
|
const DRAFT_DEBOUNCE_MS = 300;
|
|
70
70
|
|
|
71
|
+
// ---------- decisions on the wire ----------
|
|
72
|
+
// A decision POST answers with one of a small set of outcomes, and the page
|
|
73
|
+
// reacts to each differently: a busy deck keeps the answer and re-sends it, a
|
|
74
|
+
// stopped workspace waits for the heartbeat, a newer version reloads, a 401 is a
|
|
75
|
+
// sign-out. These used to collapse into "any 409 = round superseded" and "any
|
|
76
|
+
// failure = alert()", which locked the composer for good and lost answers.
|
|
77
|
+
// Both gates (standalone and embedded) read the same table.
|
|
78
|
+
const DECISION_CODES = new Set([
|
|
79
|
+
"stale_subject",
|
|
80
|
+
"local_conflict", // the record moved under the page: a newer version, like stale_subject
|
|
81
|
+
"deck_busy",
|
|
82
|
+
"approval_in_progress", // another approval is being written: retried like deck_busy
|
|
83
|
+
"already_answered",
|
|
84
|
+
"validation_failed",
|
|
85
|
+
"unauthorized",
|
|
86
|
+
]);
|
|
87
|
+
// Refusals the runner re-sends by itself (same request, same operationId).
|
|
88
|
+
const RETRY_CODES = new Set(["deck_busy", "approval_in_progress"]);
|
|
89
|
+
const BUSY_RETRY_MS = 3000;
|
|
90
|
+
const OFFLINE_RETRY_MS = 5000;
|
|
91
|
+
|
|
92
|
+
// One id per decision ATTEMPT, reused on every retry of it: the server answers a
|
|
93
|
+
// repeated id idempotently, so a retry after a lost response can never answer
|
|
94
|
+
// twice.
|
|
95
|
+
export function newOperationId() {
|
|
96
|
+
try {
|
|
97
|
+
if (globalThis.crypto && typeof globalThis.crypto.randomUUID === "function") return globalThis.crypto.randomUUID();
|
|
98
|
+
} catch {
|
|
99
|
+
/* fall through to the hand-rolled v4 */
|
|
100
|
+
}
|
|
101
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (ch) {
|
|
102
|
+
const r = (Math.random() * 16) | 0;
|
|
103
|
+
return (ch === "x" ? r : (r & 0x3) | 0x8).toString(16);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// `status` + parsed body → { ok: true, ack } | { ok: false, code, message, retryable, details, status }.
|
|
108
|
+
// `body` may be null (no JSON); a network failure is classified by the caller as
|
|
109
|
+
// { ok:false, code:"offline" }. The standalone gate answers a settled run with
|
|
110
|
+
// `{ error: "round_settled" }` (a plain string): another window already decided.
|
|
111
|
+
export function classifyDecision(status, body) {
|
|
112
|
+
if (status >= 200 && status < 300) return { ok: true, ack: body && typeof body === "object" ? body : {} };
|
|
113
|
+
const err = body && body.error;
|
|
114
|
+
const raw = err && typeof err === "object" ? err.code : typeof err === "string" ? err : null;
|
|
115
|
+
const out = {
|
|
116
|
+
ok: false,
|
|
117
|
+
status,
|
|
118
|
+
code: raw || null,
|
|
119
|
+
message: err && typeof err === "object" && typeof err.message === "string" ? err.message : "",
|
|
120
|
+
retryable: !!(err && typeof err === "object" && err.retryable),
|
|
121
|
+
details: err && typeof err === "object" && err.details ? err.details : null,
|
|
122
|
+
};
|
|
123
|
+
// Only an EXPLICIT already_answered (or the standalone gate's round_settled)
|
|
124
|
+
// means "answered elsewhere": any other 409 (operation_mismatch,
|
|
125
|
+
// outcome_unknown, unsupported_format, …) is a failure that keeps the drafts
|
|
126
|
+
// and shows the server's words — never a silent switch to "answered".
|
|
127
|
+
if (status === 401) out.code = "unauthorized";
|
|
128
|
+
else if (raw === "round_settled") out.code = "already_answered";
|
|
129
|
+
else if (DECISION_CODES.has(raw)) out.code = raw;
|
|
130
|
+
else if (status >= 500 || raw === "workspace_stopping" || raw === "stopping") out.code = "offline";
|
|
131
|
+
else if (status === 422) out.code = "validation_failed";
|
|
132
|
+
else out.code = raw || "failed";
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// One heartbeat answer → the connection state + the beat. A foreign service on
|
|
137
|
+
// the port (or a non-JSON body) is NOT the workspace answering: it reads as
|
|
138
|
+
// offline, never as "the agent stopped listening".
|
|
139
|
+
export function readBeat(status, body, service) {
|
|
140
|
+
if (status === 401) return { conn: "signedout", beat: null };
|
|
141
|
+
if (!(status >= 200 && status < 300) || !body || typeof body !== "object") return { conn: "offline", beat: null };
|
|
142
|
+
if (service && body.service && body.service !== service) return { conn: "offline", beat: null };
|
|
143
|
+
return { conn: "online", beat: body };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// When the agent received the answer to `taskId`, or null.
|
|
147
|
+
export function pickedUpAt(beat, taskId) {
|
|
148
|
+
const a = beat && beat.answered;
|
|
149
|
+
if (!a || !taskId || a.taskId !== taskId) return null;
|
|
150
|
+
return a.pickedUpAt || null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// The decision state machine: one attempt at a time; a busy deck re-sends the
|
|
154
|
+
// SAME request every 3 s, a stopped workspace re-sends it on `retryNow()` (the
|
|
155
|
+
// heartbeat came back) or every 5 s, everything else ends the attempt.
|
|
156
|
+
// `handlers`: onSuccess(ack) · onBusy({ since, elapsedMs, details }) ·
|
|
157
|
+
// onOffline(outcome) · onFailed(outcome). `retryOffline: false` (the standalone
|
|
158
|
+
// gate — its server is the command, and it does not come back) ends an offline
|
|
159
|
+
// attempt as a failure instead of waiting.
|
|
160
|
+
export function createDecisionRunner(opts) {
|
|
161
|
+
const doFetch = opts.fetch;
|
|
162
|
+
const endpoint = opts.endpoint;
|
|
163
|
+
// Wrapped, never stored bare: a browser's setTimeout called as a method of
|
|
164
|
+
// another object throws "Illegal invocation".
|
|
165
|
+
const timers = opts.timers || {
|
|
166
|
+
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
167
|
+
clearTimeout: (id) => clearTimeout(id),
|
|
168
|
+
};
|
|
169
|
+
const now = opts.now || Date.now;
|
|
170
|
+
const busyMs = opts.busyRetryMs || BUSY_RETRY_MS;
|
|
171
|
+
const offlineMs = opts.offlineRetryMs || OFFLINE_RETRY_MS;
|
|
172
|
+
const retryOffline = opts.retryOffline !== false;
|
|
173
|
+
let current = null;
|
|
174
|
+
|
|
175
|
+
function finish(c) {
|
|
176
|
+
if (c.timer) timers.clearTimeout(c.timer);
|
|
177
|
+
c.timer = null;
|
|
178
|
+
if (current === c) current = null;
|
|
179
|
+
}
|
|
180
|
+
function schedule(c, ms) {
|
|
181
|
+
if (c.timer) timers.clearTimeout(c.timer);
|
|
182
|
+
c.timer = timers.setTimeout(function () {
|
|
183
|
+
c.timer = null;
|
|
184
|
+
send(c);
|
|
185
|
+
}, ms);
|
|
186
|
+
}
|
|
187
|
+
function send(c) {
|
|
188
|
+
if (current !== c || c.inFlight) return;
|
|
189
|
+
c.inFlight = true;
|
|
190
|
+
c.state = "sending";
|
|
191
|
+
let p;
|
|
192
|
+
try {
|
|
193
|
+
p = doFetch(endpoint, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: { "Content-Type": "application/json" },
|
|
196
|
+
credentials: "same-origin",
|
|
197
|
+
body: JSON.stringify(c.attempt.body),
|
|
198
|
+
});
|
|
199
|
+
} catch (e) {
|
|
200
|
+
p = Promise.reject(e);
|
|
201
|
+
}
|
|
202
|
+
Promise.resolve(p)
|
|
203
|
+
.then(
|
|
204
|
+
function (res) {
|
|
205
|
+
return Promise.resolve(res.json ? res.json() : null)
|
|
206
|
+
.catch(function () {
|
|
207
|
+
return null;
|
|
208
|
+
})
|
|
209
|
+
.then(function (body) {
|
|
210
|
+
return classifyDecision(res.status, body);
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
function () {
|
|
214
|
+
return { ok: false, code: "offline", status: 0, message: "", retryable: true, details: null, network: true };
|
|
215
|
+
},
|
|
216
|
+
)
|
|
217
|
+
.then(function (o) {
|
|
218
|
+
c.inFlight = false;
|
|
219
|
+
handle(c, o);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
function handle(c, o) {
|
|
223
|
+
if (current !== c) return; // cancelled meanwhile
|
|
224
|
+
if (o.ok) {
|
|
225
|
+
finish(c);
|
|
226
|
+
c.handlers.onSuccess(o.ack);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (RETRY_CODES.has(o.code)) {
|
|
230
|
+
if (!c.busySince) c.busySince = now();
|
|
231
|
+
c.state = "busy";
|
|
232
|
+
c.handlers.onBusy({ since: c.busySince, elapsedMs: now() - c.busySince, details: o.details });
|
|
233
|
+
schedule(c, busyMs);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (o.code === "offline" && retryOffline) {
|
|
237
|
+
c.state = "offline";
|
|
238
|
+
c.handlers.onOffline(o);
|
|
239
|
+
schedule(c, offlineMs);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
finish(c);
|
|
243
|
+
c.handlers.onFailed(o);
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
start(attempt, handlers) {
|
|
247
|
+
if (current) return false;
|
|
248
|
+
current = { attempt: attempt, handlers: handlers, timer: null, busySince: null, state: "sending", inFlight: false };
|
|
249
|
+
send(current);
|
|
250
|
+
return true;
|
|
251
|
+
},
|
|
252
|
+
// The heartbeat answered again: re-send a waiting attempt right away.
|
|
253
|
+
retryNow() {
|
|
254
|
+
const c = current;
|
|
255
|
+
if (!c || c.inFlight || (c.state !== "offline" && c.state !== "busy")) return false;
|
|
256
|
+
if (c.timer) timers.clearTimeout(c.timer);
|
|
257
|
+
c.timer = null;
|
|
258
|
+
send(c);
|
|
259
|
+
return true;
|
|
260
|
+
},
|
|
261
|
+
cancel() {
|
|
262
|
+
if (current) finish(current);
|
|
263
|
+
},
|
|
264
|
+
pending() {
|
|
265
|
+
return current ? current.attempt : null;
|
|
266
|
+
},
|
|
267
|
+
state() {
|
|
268
|
+
return current ? current.state : null;
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
71
273
|
export function createGateSession(cfg) {
|
|
72
274
|
const name = cfg.name; // "review" | "plan" — storage prefix + wire shape
|
|
73
275
|
const deck = cfg.deck || "";
|
|
@@ -79,9 +281,24 @@ export function createGateSession(cfg) {
|
|
|
79
281
|
// would differ by definition).
|
|
80
282
|
const LEDGER_KEY = "slidemind-" + name + "-ledger:" + deck;
|
|
81
283
|
const CTX_KEY = "slidemind-" + name + "-ctx:" + deck;
|
|
82
|
-
|
|
284
|
+
// Per deck as well: the workspace page frames several decks' gates in ONE tab,
|
|
285
|
+
// and a tab's sessionStorage is shared by all of them.
|
|
286
|
+
const ROUND_READY_FLAG = "slidemind-" + name + "-round-ready:" + deck;
|
|
287
|
+
// A one-line message the NEXT load shows ("here is the new version, your
|
|
288
|
+
// comments are kept") — the reload that makes it true wipes the JS context.
|
|
289
|
+
const NOTICE_KEY = "slidemind-" + name + "-notice:" + deck;
|
|
83
290
|
let draftTimer = null;
|
|
84
291
|
let pollTimer = null;
|
|
292
|
+
// The decision wire. Embedded, a stopped workspace comes back (the daemon
|
|
293
|
+
// restarts), so an offline attempt waits for it; the standalone gate's server
|
|
294
|
+
// IS the command, so an offline attempt there ends.
|
|
295
|
+
const runner = createDecisionRunner({
|
|
296
|
+
fetch: doFetch,
|
|
297
|
+
endpoint: cfg.endpoint,
|
|
298
|
+
timers: cfg.timers,
|
|
299
|
+
now: cfg.now,
|
|
300
|
+
retryOffline: !!cfg.embedded,
|
|
301
|
+
});
|
|
85
302
|
|
|
86
303
|
function read(key, fallback) {
|
|
87
304
|
try {
|
|
@@ -110,21 +327,25 @@ export function createGateSession(cfg) {
|
|
|
110
327
|
clearTimeout(draftTimer);
|
|
111
328
|
draftTimer = null;
|
|
112
329
|
}
|
|
113
|
-
write(CTX_KEY, Object.assign({ nonce: cfg.round || "" }, extra || {}));
|
|
330
|
+
write(CTX_KEY, Object.assign({ nonce: cfg.round || "", subject: cfg.subject || null }, extra || {}));
|
|
114
331
|
}
|
|
115
332
|
// Same write, coalesced — for the per-keystroke callers.
|
|
116
333
|
function saveCtxSoon(extra) {
|
|
117
334
|
if (draftTimer) clearTimeout(draftTimer);
|
|
118
335
|
draftTimer = setTimeout(function () {
|
|
119
336
|
draftTimer = null;
|
|
120
|
-
write(CTX_KEY, Object.assign({ nonce: cfg.round || "" }, extra || {}));
|
|
337
|
+
write(CTX_KEY, Object.assign({ nonce: cfg.round || "", subject: cfg.subject || null }, extra || {}));
|
|
121
338
|
}, DRAFT_DEBOUNCE_MS);
|
|
122
339
|
}
|
|
123
340
|
|
|
124
341
|
// Restore this tab's session and work out whether THIS load is a new round.
|
|
125
342
|
// Returns the ledger plus the round state; the caller applies its own ctx
|
|
126
343
|
// fields from the returned `ctx` and its own round-arrival cosmetics.
|
|
127
|
-
|
|
344
|
+
// `opts.hold(ctx)` may keep a changed nonce from counting as a new round: in
|
|
345
|
+
// the workspace the agent's edits change the content (and so the nonce) while
|
|
346
|
+
// it is still revising, and that is not the next round — the agent has not
|
|
347
|
+
// asked yet, so nothing it was handed is "addressed".
|
|
348
|
+
function restore(seed, opts) {
|
|
128
349
|
let ledger = read(LEDGER_KEY, []) || [];
|
|
129
350
|
if (!Array.isArray(ledger)) ledger = [];
|
|
130
351
|
// A tab left open across the upgrade from the queueing release still holds
|
|
@@ -138,15 +359,26 @@ export function createGateSession(cfg) {
|
|
|
138
359
|
let roundNo = ctx && typeof ctx.round === "number" ? ctx.round : 1;
|
|
139
360
|
// A different SERVER nonce = a genuinely newer run served this page, i.e. a
|
|
140
361
|
// new round. Same nonce = a plain refresh of the round already on screen.
|
|
141
|
-
const
|
|
362
|
+
const nonceMoved = !!(ctx && ctx.nonce && cfg.round && ctx.nonce !== cfg.round);
|
|
363
|
+
const held = nonceMoved && !!(opts && typeof opts.hold === "function" && opts.hold(ctx));
|
|
364
|
+
const newRound = nonceMoved && !held;
|
|
365
|
+
// The agent asked again on BYTE-IDENTICAL content (it read the comments and
|
|
366
|
+
// changed nothing): a new round, but nothing was worked in — the comments it
|
|
367
|
+
// was handed stay "sent", and the page must not say "updated".
|
|
368
|
+
const unchanged = newRound && !!(ctx && ctx.subject && cfg.subject && ctx.subject === cfg.subject);
|
|
369
|
+
const hadSent = ledger.some(function (c) {
|
|
370
|
+
return c.status === "sent";
|
|
371
|
+
});
|
|
142
372
|
if (newRound) {
|
|
143
373
|
roundNo += 1;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
c.status
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
374
|
+
if (!unchanged) {
|
|
375
|
+
ledger.forEach(function (c) {
|
|
376
|
+
if (c.status === "sent") {
|
|
377
|
+
c.status = "addressed";
|
|
378
|
+
c.addressedInRound = roundNo;
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
}
|
|
150
382
|
}
|
|
151
383
|
// Seed a tab with no session of its own (a fresh window) from the deck's
|
|
152
384
|
// persisted round. These are the rows the agent has JUST been handed — they
|
|
@@ -165,7 +397,22 @@ export function createGateSession(cfg) {
|
|
|
165
397
|
// sent this round; approving the same render again would only 409), and
|
|
166
398
|
// ends the moment a newer run serves the page.
|
|
167
399
|
const working = !newRound && !!(ctx && ctx.working);
|
|
168
|
-
return { ledger, roundNo, working, newRound, ctx };
|
|
400
|
+
return { ledger, roundNo, working, newRound, held, unchanged, hadSent, ctx };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// The one-line message for the next load (see NOTICE_KEY): written right
|
|
404
|
+
// before a reload, consumed once at boot.
|
|
405
|
+
function noteForNextLoad(note) {
|
|
406
|
+
write(NOTICE_KEY, note);
|
|
407
|
+
}
|
|
408
|
+
function consumeNote() {
|
|
409
|
+
const note = read(NOTICE_KEY, null);
|
|
410
|
+
try {
|
|
411
|
+
store.removeItem(NOTICE_KEY);
|
|
412
|
+
} catch {
|
|
413
|
+
/* storage blocked — nothing was stored either */
|
|
414
|
+
}
|
|
415
|
+
return note && typeof note === "object" ? note : null;
|
|
169
416
|
}
|
|
170
417
|
|
|
171
418
|
// Where a row stands, for the ledger rail / sidebar chip.
|
|
@@ -265,6 +512,9 @@ export function createGateSession(cfg) {
|
|
|
265
512
|
// a tab buried in a background window must not swallow the round invisibly,
|
|
266
513
|
// so the server opens a fresh browser tab and answers `action: "close"` here,
|
|
267
514
|
// which retires this tab rather than reloading it into a hidden duplicate.
|
|
515
|
+
// STANDALONE gates only: inside the workspace page the shell's heartbeat
|
|
516
|
+
// monitor watches the round for the page's whole life (it never times out —
|
|
517
|
+
// the workspace page has no "session ended").
|
|
268
518
|
function startNextRoundPolling(handlers) {
|
|
269
519
|
if (pollTimer) return; // one poll per page life (working mode can re-enter)
|
|
270
520
|
const startedAt = Date.now();
|
|
@@ -331,5 +581,16 @@ export function createGateSession(cfg) {
|
|
|
331
581
|
consumeRoundFlag,
|
|
332
582
|
announceNewRound,
|
|
333
583
|
startNextRoundPolling,
|
|
584
|
+
noteForNextLoad,
|
|
585
|
+
consumeNote,
|
|
586
|
+
// the decision wire (see createDecisionRunner)
|
|
587
|
+
decide: runner.start,
|
|
588
|
+
retryDecision: runner.retryNow,
|
|
589
|
+
cancelDecision: runner.cancel,
|
|
590
|
+
decisionPending: runner.pending,
|
|
591
|
+
decisionState: runner.state,
|
|
592
|
+
newOperationId,
|
|
593
|
+
readBeat,
|
|
594
|
+
pickedUpAt,
|
|
334
595
|
};
|
|
335
596
|
}
|