scenescout 3.23.2 → 3.23.3
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/CHANGELOG.md +7 -0
- package/dist/engine/live-page.js +8 -5
- package/dist/engine/replay.js +34 -0
- package/dist/engine/report.js +3 -2
- package/dist/mcp-server.js +29 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# scenescout
|
|
2
2
|
|
|
3
|
+
## 3.23.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 43012d7: The live board's report panel keeps showing a finding's pictures when sessions in two project folders are attached at once and the session that filed the finding has closed. The report now names the project it describes, and the board loads its pictures from that folder rather than from whichever project is attached first.
|
|
8
|
+
- 1cda780: The live view's run page keeps showing its pictures after a session attaches to another project folder. Its picture addresses now name the project the page was written for, so a page already open in a browser loads them from that folder rather than from whichever project attached last.
|
|
9
|
+
|
|
3
10
|
## 3.23.2
|
|
4
11
|
|
|
5
12
|
### Patch Changes
|
package/dist/engine/live-page.js
CHANGED
|
@@ -297,6 +297,8 @@ export const LIVE_PAGE = `<!doctype html>
|
|
|
297
297
|
var reportKey = null;
|
|
298
298
|
// Per-finding frames from api/report: empty unless the run was recorded.
|
|
299
299
|
var reportEvidence = [];
|
|
300
|
+
// Where the report's pictures are served from: the project it was rendered from (api/report names it).
|
|
301
|
+
var reportFramePrefix = 'record/';
|
|
300
302
|
var savedACopy = false;
|
|
301
303
|
// A result that reads as a failure is shown in red.
|
|
302
304
|
var BAD_RESULT = /error|fail|refus|block|violation|abandoned/i;
|
|
@@ -509,13 +511,13 @@ export const LIVE_PAGE = `<!doctype html>
|
|
|
509
511
|
picture = el('figure', 'picture');
|
|
510
512
|
picture.setAttribute('data-testid', 'live-report-picture-' + id);
|
|
511
513
|
var link = el('a');
|
|
512
|
-
link.href =
|
|
514
|
+
link.href = reportFramePrefix + found.picture.file;
|
|
513
515
|
link.target = '_blank';
|
|
514
516
|
link.rel = 'noreferrer';
|
|
515
517
|
link.setAttribute('data-testid', 'live-report-picture-open');
|
|
516
518
|
var pic = el('img');
|
|
517
519
|
pic.loading = 'lazy';
|
|
518
|
-
pic.src =
|
|
520
|
+
pic.src = reportFramePrefix + found.picture.file;
|
|
519
521
|
pic.alt = 'What the finding is about, when it was filed';
|
|
520
522
|
pic.addEventListener('error', function () { picture.classList.add('gone'); });
|
|
521
523
|
link.appendChild(pic);
|
|
@@ -533,7 +535,7 @@ export const LIVE_PAGE = `<!doctype html>
|
|
|
533
535
|
var fig = el('figure');
|
|
534
536
|
var img = el('img');
|
|
535
537
|
img.loading = 'lazy';
|
|
536
|
-
img.src =
|
|
538
|
+
img.src = reportFramePrefix + f.frame;
|
|
537
539
|
img.alt = f.action + ' ' + f.detail;
|
|
538
540
|
// A recording deleted since the run leaves a broken icon under a finding
|
|
539
541
|
// that still counts it as evidence. Say which it is.
|
|
@@ -607,7 +609,7 @@ export const LIVE_PAGE = `<!doctype html>
|
|
|
607
609
|
var fig = el('figure', 'picture');
|
|
608
610
|
var pic = el('img');
|
|
609
611
|
pic.loading = 'lazy';
|
|
610
|
-
pic.src =
|
|
612
|
+
pic.src = reportFramePrefix + m[2];
|
|
611
613
|
pic.alt = m[1];
|
|
612
614
|
fig.appendChild(pic);
|
|
613
615
|
container.appendChild(fig);
|
|
@@ -680,12 +682,13 @@ export const LIVE_PAGE = `<!doctype html>
|
|
|
680
682
|
}
|
|
681
683
|
reportMarkdown = d.markdown;
|
|
682
684
|
reportEvidence = d.evidence || [];
|
|
685
|
+
reportFramePrefix = d.framePrefix || 'record/';
|
|
683
686
|
meta.textContent = (finished ? 'as the run left it at ' : 'as the run stands at ') + clock(d.at) + ' · ' + whereItIs();
|
|
684
687
|
// The report is re-read every few seconds while the run goes on.
|
|
685
688
|
// Re-rendering an unchanged document threw away what the reader was
|
|
686
689
|
// doing with it: an opened accordion shut itself, and the page jumped
|
|
687
690
|
// back to the top, every five seconds.
|
|
688
|
-
var key = d.markdown + '\\u0000' + JSON.stringify(reportEvidence);
|
|
691
|
+
var key = d.markdown + '\\u0000' + JSON.stringify(reportEvidence) + '\\u0000' + reportFramePrefix;
|
|
689
692
|
if (key === reportKey) return;
|
|
690
693
|
var open = {};
|
|
691
694
|
var was = doc.querySelectorAll('details[open][data-testid]');
|
package/dist/engine/replay.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* Everything here is pure string work — no browser, no filesystem — so the
|
|
19
19
|
* escaping and the grouping are table-tested.
|
|
20
20
|
*/
|
|
21
|
+
import crypto from "node:crypto";
|
|
21
22
|
import path from "node:path";
|
|
22
23
|
import { isSafeRelativePath } from "./plain.js";
|
|
23
24
|
/** Most frames one recorded session keeps. A long run is thousands of actions, and a project folder is not a video store. */
|
|
@@ -59,6 +60,39 @@ export function resolveFrame(root, relPath) {
|
|
|
59
60
|
const file = path.resolve(base, inside);
|
|
60
61
|
return file.startsWith(base + path.sep) ? file : null;
|
|
61
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* A short name for a project folder, used in the live view's frame addresses so
|
|
65
|
+
* a page says which project its pictures belong to. One engine can serve runs
|
|
66
|
+
* in several folders in turn, and a page already open in a browser loads its
|
|
67
|
+
* images later, after another folder may have attached. The name is derived
|
|
68
|
+
* from the folder's resolved path and reveals nothing of it.
|
|
69
|
+
*/
|
|
70
|
+
export function projectKey(dir) {
|
|
71
|
+
return "p-" + crypto.createHash("sha256").update(path.resolve(dir)).digest("hex").slice(0, 12);
|
|
72
|
+
}
|
|
73
|
+
/** A frame address split into the project it names, when it names one, and the path inside that project's recordings. */
|
|
74
|
+
export function splitProjectFrame(relPath) {
|
|
75
|
+
const m = /^(p-[0-9a-f]{12})\/(.+)$/.exec(relPath);
|
|
76
|
+
return m ? { key: m[1], rest: m[2] } : { rest: relPath };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The project folders this process has rendered a run page or a board report
|
|
80
|
+
* for, by key. Only a folder one was rendered from can be named, so a viewer
|
|
81
|
+
* cannot reach any other folder by guessing a key.
|
|
82
|
+
*/
|
|
83
|
+
export class ProjectDirs {
|
|
84
|
+
byKey = new Map();
|
|
85
|
+
/** Remember `dir` and return its key. */
|
|
86
|
+
remember(dir) {
|
|
87
|
+
const key = projectKey(dir);
|
|
88
|
+
this.byKey.set(key, dir);
|
|
89
|
+
return key;
|
|
90
|
+
}
|
|
91
|
+
/** The folder a key names, or undefined when nothing was rendered from one with that key. */
|
|
92
|
+
dir(key) {
|
|
93
|
+
return this.byKey.get(key);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
62
96
|
/** Text from the app under test reaches this document, so nothing is interpolated unescaped. */
|
|
63
97
|
export function escapeHtml(text) {
|
|
64
98
|
return text.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
package/dist/engine/report.js
CHANGED
|
@@ -4,7 +4,7 @@ import { baseRoute, reachedRoutes, routeIdentity, isEmbedKey, isWorthALook, judg
|
|
|
4
4
|
import { sayVerification } from "./verify.js";
|
|
5
5
|
import { describePicture, readFindingPicture } from "./capture.js";
|
|
6
6
|
import { feedForSession } from "./live.js";
|
|
7
|
-
import { buildReplayHtml, evidenceFor } from "./replay.js";
|
|
7
|
+
import { buildReplayHtml, evidenceFor, projectKey } from "./replay.js";
|
|
8
8
|
import { calibrate, formatCalibration } from "./calibration.js";
|
|
9
9
|
import { formatPace, measurePace } from "./pace.js";
|
|
10
10
|
import { formatNeverSubmittedEmpty } from "./forms.js";
|
|
@@ -221,7 +221,8 @@ export function replayDocument(memory, markdown, version = "") {
|
|
|
221
221
|
project: projectName(memory.dir),
|
|
222
222
|
at: new Date().toISOString(),
|
|
223
223
|
version,
|
|
224
|
-
|
|
224
|
+
// Keyed by project, so the page's pictures still load after a session attaches to another folder (replay.ts).
|
|
225
|
+
framePrefix: `record/${projectKey(memory.dir)}/`,
|
|
225
226
|
savedAt: memory.dir,
|
|
226
227
|
});
|
|
227
228
|
}
|
package/dist/mcp-server.js
CHANGED
|
@@ -70,7 +70,7 @@ import { CRITERION_VERDICTS, findCriterion, formatCriteriaForLanes, formatReadin
|
|
|
70
70
|
import { ACTION_TIMEOUT_ENV, DEFAULT_ACTION_TIMEOUT_MS, DEFAULT_CRAWL_NAV_TIMEOUT_MS, DEFAULT_NAV_TIMEOUT_MS, LIMIT_BOUNDS, NAV_TIMEOUT_ENV, watchdogFor, } from "./engine/limits.js";
|
|
71
71
|
import { MAX_READ_POSTS, READ_POSTS_ENV } from "./engine/policy.js";
|
|
72
72
|
import { chooseProjectFolder, PROJECTS_DIR_ENV, workspaceFromRoots } from "./engine/project-folder.js";
|
|
73
|
-
import { RECORD_MAX_FRAMES, resolveFrame } from "./engine/replay.js";
|
|
73
|
+
import { ProjectDirs, RECORD_MAX_FRAMES, resolveFrame, splitProjectFrame } from "./engine/replay.js";
|
|
74
74
|
import { describePace, normalizePace } from "./engine/settle.js";
|
|
75
75
|
import { needsTask, taskRefusal, TASK_MAX } from "./engine/task.js";
|
|
76
76
|
import { formElicitationSupported, INTAKE_FORM_MESSAGE, INTAKE_TOOL, intakeFormSchema, intakeFromForm, intakeReplyText } from "./intake.js";
|
|
@@ -149,6 +149,8 @@ let liveError = null;
|
|
|
149
149
|
* found — which is the moment somebody most wants to read it.
|
|
150
150
|
*/
|
|
151
151
|
let lastRun = null;
|
|
152
|
+
/** Every project folder a run page or the board's report has been rendered from, so their pictures resolve against it later (#457, #463). */
|
|
153
|
+
const pageProjects = new ProjectDirs();
|
|
152
154
|
/** Render the report for a session that is about to close, so the live view keeps it. */
|
|
153
155
|
function keepReport(eng) {
|
|
154
156
|
if (!eng.memory)
|
|
@@ -167,6 +169,7 @@ function keepReport(eng) {
|
|
|
167
169
|
}
|
|
168
170
|
try {
|
|
169
171
|
lastRun.evidence = reportEvidence(eng.memory);
|
|
172
|
+
pageProjects.remember(eng.memory.dir);
|
|
170
173
|
lastRun.replay = replayDocument(eng.memory, lastRun.markdown, PKG_VERSION);
|
|
171
174
|
}
|
|
172
175
|
catch (err) {
|
|
@@ -185,6 +188,10 @@ function reportFile(dir) {
|
|
|
185
188
|
return { path: file, written: false };
|
|
186
189
|
}
|
|
187
190
|
}
|
|
191
|
+
/** The address a report's frames start with: keyed by project, and the folder is remembered so the key resolves (#463). */
|
|
192
|
+
function framePrefixFor(dir) {
|
|
193
|
+
return `record/${pageProjects.remember(dir)}/`;
|
|
194
|
+
}
|
|
188
195
|
const liveProvider = {
|
|
189
196
|
snapshot: () => ({
|
|
190
197
|
pid: process.pid,
|
|
@@ -204,9 +211,14 @@ const liveProvider = {
|
|
|
204
211
|
report: () => {
|
|
205
212
|
const eng = (lastWriter && engines.get(lastWriter.session)) ?? engines.values().next().value;
|
|
206
213
|
if (!eng?.memory)
|
|
207
|
-
return lastRun ? { markdown: lastRun.markdown, at: lastRun.at, evidence: lastRun.evidence } : null;
|
|
214
|
+
return lastRun ? { markdown: lastRun.markdown, at: lastRun.at, evidence: lastRun.evidence, framePrefix: framePrefixFor(lastRun.dir) } : null;
|
|
208
215
|
const { markdown } = generateReport(eng.memory, eng.oracleLog.all, reportExtras(eng), { write: false });
|
|
209
|
-
return {
|
|
216
|
+
return {
|
|
217
|
+
markdown: redactSecrets(markdown),
|
|
218
|
+
at: new Date().toISOString(),
|
|
219
|
+
evidence: reportEvidence(eng.memory),
|
|
220
|
+
framePrefix: framePrefixFor(eng.memory.dir),
|
|
221
|
+
};
|
|
210
222
|
},
|
|
211
223
|
/**
|
|
212
224
|
* The whole run as one page, served at its own address. The live view sends
|
|
@@ -219,6 +231,7 @@ const liveProvider = {
|
|
|
219
231
|
return lastRun?.replay || null;
|
|
220
232
|
try {
|
|
221
233
|
const { markdown } = generateReport(eng.memory, eng.oracleLog.all, reportExtras(eng), { write: false });
|
|
234
|
+
pageProjects.remember(eng.memory.dir);
|
|
222
235
|
return replayDocument(eng.memory, redactSecrets(markdown), PKG_VERSION);
|
|
223
236
|
}
|
|
224
237
|
catch (err) {
|
|
@@ -233,12 +246,19 @@ const liveProvider = {
|
|
|
233
246
|
* comes from a viewer, so it is resolved and then required to still be
|
|
234
247
|
* inside that directory: nothing else in the project is reachable this way.
|
|
235
248
|
*/
|
|
236
|
-
frame: async (
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
const
|
|
249
|
+
frame: async (address) => {
|
|
250
|
+
// A run page names the project it was rendered from, so its pictures load
|
|
251
|
+
// after a session attaches to another folder. The live board's own frames
|
|
252
|
+
// carry no project: they belong to the session their path names, and
|
|
253
|
+
// falling back keeps a finished run's frames reachable.
|
|
254
|
+
const { key, rest: relPath } = splitProjectFrame(address);
|
|
255
|
+
let dir;
|
|
256
|
+
if (key)
|
|
257
|
+
dir = pageProjects.dir(key);
|
|
258
|
+
else {
|
|
259
|
+
const named = relPath.replace(/^recordings[\\/]/, "").split("/")[0];
|
|
260
|
+
dir = engines.get(named)?.memory?.dir ?? engines.values().next().value?.memory?.dir ?? lastRun?.dir;
|
|
261
|
+
}
|
|
242
262
|
if (!dir)
|
|
243
263
|
return null;
|
|
244
264
|
const file = resolveFrame(path.join(dir, "recordings"), relPath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scenescout",
|
|
3
|
-
"version": "3.23.
|
|
3
|
+
"version": "3.23.3",
|
|
4
4
|
"description": "SceneScout — exploratory UI testing for AI coding agents. An MCP server that gives any agent (Claude Code, Cursor, VS Code Copilot, Codex, Gemini CLI and others) a structured view of a running web app, always-on oracles, a network-level write policy, memory across runs and a gap-checked report.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "brunoboto96",
|