scenescout 1.4.0 → 1.5.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/CHANGELOG.md +6 -0
- package/dist/engine/live-page.js +71 -2
- package/dist/mcp-server.js +42 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# scenescout
|
|
2
2
|
|
|
3
|
+
## 1.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 791b4d0: The live view now hands over the report when the run ends. Closing the last session used to take the report with it — the view served it from the live engine, so the moment the browsers went the page said there was nothing to report. The last rendering is now kept, and when the board empties the page says the run has finished, opens the report by itself, and names the file it belongs in: `saved at <path>` once `scout_report` has written it, or plainly that it is not on disk and this page holds the only copy. A **Save a copy** button downloads that copy through the viewer's own browser (nothing is asked of the engine, which still answers `GET` and nothing else), and closing the tab on a finished run whose report was never written asks for confirmation first.
|
|
8
|
+
|
|
3
9
|
## 1.4.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/dist/engine/live-page.js
CHANGED
|
@@ -48,6 +48,13 @@ export const LIVE_PAGE = `<!doctype html>
|
|
|
48
48
|
button[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
49
49
|
#banner { display: none; margin: 12px 16px 0; padding: 8px 12px; border-radius: 6px; background: var(--stuck-bg); color: var(--stuck); }
|
|
50
50
|
#empty { display: none; padding: 48px 16px; text-align: center; color: var(--muted); }
|
|
51
|
+
#finished { display: none; max-width: 640px; margin: 48px auto; padding: 24px; text-align: center;
|
|
52
|
+
background: var(--panel); border: 1px solid var(--line); border-radius: 8px; }
|
|
53
|
+
#finished.open { display: block; }
|
|
54
|
+
#finished h2 { margin: 0 0 8px; font-size: 18px; }
|
|
55
|
+
#finished p { margin: 0 0 8px; color: var(--muted); }
|
|
56
|
+
#finished .where { font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; overflow-wrap: anywhere; }
|
|
57
|
+
#finished button { margin-top: 8px; padding: 7px 14px; font-weight: 600; }
|
|
51
58
|
main { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr)); gap: 12px; padding: 16px; }
|
|
52
59
|
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; display: flex; flex-direction: column; }
|
|
53
60
|
.card.stuck { border-color: var(--stuck); }
|
|
@@ -143,12 +150,19 @@ export const LIVE_PAGE = `<!doctype html>
|
|
|
143
150
|
</header>
|
|
144
151
|
<div id="banner" role="alert" data-testid="live-unreachable-banner">The engine is not answering. It may have exited; this page will pick up again if it comes back.</div>
|
|
145
152
|
<div id="empty" data-testid="live-empty-state">No session is attached yet. Cards appear here as soon as one attaches.</div>
|
|
153
|
+
<div id="finished" data-testid="live-finished-state">
|
|
154
|
+
<h2>The run has finished</h2>
|
|
155
|
+
<p>Its browsers are closed, so there is nothing left to watch. What it found is in the report.</p>
|
|
156
|
+
<p class="where" id="finished-where" data-testid="live-finished-where"></p>
|
|
157
|
+
<button type="button" id="finished-report" data-testid="live-finished-report">Read the report</button>
|
|
158
|
+
</div>
|
|
146
159
|
<main id="grid"></main>
|
|
147
160
|
<div id="report" role="dialog" aria-modal="true" aria-label="The run's report" data-testid="live-report-dialog">
|
|
148
161
|
<div class="bar">
|
|
149
162
|
<strong>Report</strong>
|
|
150
163
|
<span class="meta" id="report-meta" data-testid="live-report-meta"></span>
|
|
151
164
|
<span class="spacer"></span>
|
|
165
|
+
<button type="button" id="report-save" data-testid="live-report-save">Save a copy</button>
|
|
152
166
|
<button type="button" id="report-close" data-testid="live-report-close">Close</button>
|
|
153
167
|
</div>
|
|
154
168
|
<div class="doc" id="report-doc" data-testid="live-report-doc"></div>
|
|
@@ -187,6 +201,14 @@ export const LIVE_PAGE = `<!doctype html>
|
|
|
187
201
|
var reportOpen = false;
|
|
188
202
|
var reportTimer = null;
|
|
189
203
|
var reportProblem = null;
|
|
204
|
+
// The run had sessions and has none now: its browsers are gone, and this
|
|
205
|
+
// page holds the only rendering of the report unless it was written to disk.
|
|
206
|
+
var sawRun = false;
|
|
207
|
+
var finished = false;
|
|
208
|
+
var reportShown = false;
|
|
209
|
+
var reportFile = null;
|
|
210
|
+
var reportMarkdown = null;
|
|
211
|
+
var savedACopy = false;
|
|
190
212
|
// A result that reads as a failure is shown in red.
|
|
191
213
|
var BAD_RESULT = /error|fail|refus|block|violation|abandoned/i;
|
|
192
214
|
|
|
@@ -435,7 +457,8 @@ export const LIVE_PAGE = `<!doctype html>
|
|
|
435
457
|
meta.textContent = '';
|
|
436
458
|
return;
|
|
437
459
|
}
|
|
438
|
-
|
|
460
|
+
reportMarkdown = d.markdown;
|
|
461
|
+
meta.textContent = (finished ? 'as the run left it at ' : 'as the run stands at ') + clock(d.at) + ' · ' + whereItIs();
|
|
439
462
|
renderMarkdown(doc, d.markdown);
|
|
440
463
|
})
|
|
441
464
|
.catch(function (err) {
|
|
@@ -448,6 +471,28 @@ export const LIVE_PAGE = `<!doctype html>
|
|
|
448
471
|
}
|
|
449
472
|
});
|
|
450
473
|
}
|
|
474
|
+
/** Where the report's file is, and whether the agent has written it there. */
|
|
475
|
+
function whereItIs() {
|
|
476
|
+
if (!reportFile) return 'scout_report writes this document to .scenescout/report.md at the end';
|
|
477
|
+
if (reportFile.written) return 'saved at ' + reportFile.path;
|
|
478
|
+
return 'NOT saved: ' + reportFile.path + ' does not exist — the agent has not run scout_report, so this page holds the only copy';
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Saving is the viewer's own browser writing a file the page already has;
|
|
482
|
+
// nothing is sent to the engine, which only ever answers GET (ADR 7).
|
|
483
|
+
function saveACopy() {
|
|
484
|
+
if (!reportMarkdown) return;
|
|
485
|
+
var url = URL.createObjectURL(new Blob([reportMarkdown], { type: 'text/markdown' }));
|
|
486
|
+
var a = document.createElement('a');
|
|
487
|
+
a.href = url;
|
|
488
|
+
a.download = 'scenescout-report.md';
|
|
489
|
+
document.body.appendChild(a);
|
|
490
|
+
a.click();
|
|
491
|
+
a.remove();
|
|
492
|
+
setTimeout(function () { URL.revokeObjectURL(url); }, 10000);
|
|
493
|
+
savedACopy = true;
|
|
494
|
+
}
|
|
495
|
+
|
|
451
496
|
function openReport() {
|
|
452
497
|
reportOpen = true;
|
|
453
498
|
document.getElementById('report').classList.add('open');
|
|
@@ -608,7 +653,21 @@ export const LIVE_PAGE = `<!doctype html>
|
|
|
608
653
|
delete frames[name];
|
|
609
654
|
});
|
|
610
655
|
syncEvents();
|
|
611
|
-
|
|
656
|
+
reportFile = snap.report || reportFile;
|
|
657
|
+
if (snap.sessions.length > 0) sawRun = true;
|
|
658
|
+
finished = sawRun && snap.sessions.length === 0;
|
|
659
|
+
document.getElementById('empty').style.display = snap.sessions.length || finished ? 'none' : 'block';
|
|
660
|
+
document.getElementById('finished').classList.toggle('open', finished);
|
|
661
|
+
if (finished) {
|
|
662
|
+
var where = document.getElementById('finished-where');
|
|
663
|
+
where.textContent = whereItIs();
|
|
664
|
+
where.className = 'where' + (reportFile && reportFile.written ? '' : ' unset');
|
|
665
|
+
// The moment somebody wants the report is the moment the run ends: show it.
|
|
666
|
+
if (!reportShown) {
|
|
667
|
+
reportShown = true;
|
|
668
|
+
if (!reportOpen) openReport();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
612
671
|
document.getElementById('engine').textContent = 'engine pid ' + snap.pid + ' · v' + snap.version;
|
|
613
672
|
document.getElementById('counts').textContent = snap.sessions.length + ' session' + (snap.sessions.length === 1 ? '' : 's') +
|
|
614
673
|
' · ' + counts.running + ' running · ' + counts.idle + ' idle' + (counts.stuck ? ' · ' + counts.stuck + ' stuck' : '');
|
|
@@ -633,6 +692,16 @@ export const LIVE_PAGE = `<!doctype html>
|
|
|
633
692
|
document.getElementById('focus-feed').addEventListener('mouseleave', function () { showObjective(null); });
|
|
634
693
|
document.getElementById('focus').addEventListener('click', function (e) { if (e.target === this) closeFocus(); });
|
|
635
694
|
document.getElementById('report-open').addEventListener('click', openReport);
|
|
695
|
+
document.getElementById('finished-report').addEventListener('click', openReport);
|
|
696
|
+
document.getElementById('report-save').addEventListener('click', saveACopy);
|
|
697
|
+
// Closing the tab on a finished run whose report was never written to disk
|
|
698
|
+
// throws the only copy away. The browser shows its own confirm/dismiss, and
|
|
699
|
+
// only when the person has interacted with the page at least once.
|
|
700
|
+
window.addEventListener('beforeunload', function (e) {
|
|
701
|
+
if (!finished || savedACopy || (reportFile && reportFile.written)) return;
|
|
702
|
+
e.preventDefault();
|
|
703
|
+
e.returnValue = '';
|
|
704
|
+
});
|
|
636
705
|
document.getElementById('report-close').addEventListener('click', closeReport);
|
|
637
706
|
document.addEventListener('keydown', function (e) {
|
|
638
707
|
if (e.key !== 'Escape') return;
|
package/dist/mcp-server.js
CHANGED
|
@@ -37,7 +37,7 @@ import { reapOrphanBrowsers } from "./engine/reaper.js";
|
|
|
37
37
|
import { MemoryStore, redactSecrets } from "./engine/memory.js";
|
|
38
38
|
import { SessionQueue, withWatchdog } from "./engine/dispatch.js";
|
|
39
39
|
import { FIXTURE_KINDS } from "./engine/fixtures.js";
|
|
40
|
-
import { feedForSession, LIVE_ENV, writeStatusFile, LIVE_TOKEN_FILE, LiveServer, StatusBoard } from "./engine/live.js";
|
|
40
|
+
import { feedForSession, LIVE_ENV, writeStatusFile, LIVE_TOKEN_FILE, LiveServer, StatusBoard, } from "./engine/live.js";
|
|
41
41
|
import { computeGaps, formatRouteCoverage, generateReport } from "./engine/report.js";
|
|
42
42
|
import { EXPLORE_PROMPT_ARGUMENTS, explorePrompt, loadPlaybook, PLAYBOOK_PROMPT, PLAYBOOK_TOOL, SERVER_INSTRUCTIONS } from "./playbook.js";
|
|
43
43
|
import { formatScan, scanProject } from "./scan.js";
|
|
@@ -105,8 +105,44 @@ const liveTokenWrites = new Map();
|
|
|
105
105
|
let liveTokenWarned = false;
|
|
106
106
|
/** Why the live view could not start, when it could not: told to the agent and written to status.json. */
|
|
107
107
|
let liveError = null;
|
|
108
|
+
/**
|
|
109
|
+
* The last run's report, kept after its sessions close. The engines are gone
|
|
110
|
+
* by then, so this is the only way the live view can still show what the run
|
|
111
|
+
* found — which is the moment somebody most wants to read it.
|
|
112
|
+
*/
|
|
113
|
+
let lastRun = null;
|
|
114
|
+
/** Render the report for a session that is about to close, so the live view keeps it. */
|
|
115
|
+
function keepReport(eng) {
|
|
116
|
+
if (!eng.memory)
|
|
117
|
+
return;
|
|
118
|
+
try {
|
|
119
|
+
const { markdown } = generateReport(eng.memory, eng.oracleLog.all, reportExtras(eng), { write: false });
|
|
120
|
+
lastRun = { markdown: redactSecrets(markdown), at: new Date().toISOString(), dir: eng.memory.dir };
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Best-effort: a report that cannot be rendered must not fail a close.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Where this run's report belongs, and whether the agent has written it there yet. */
|
|
127
|
+
function reportFile(dir) {
|
|
128
|
+
if (!dir)
|
|
129
|
+
return undefined;
|
|
130
|
+
const file = path.join(dir, "report.md");
|
|
131
|
+
try {
|
|
132
|
+
return { path: file, written: fs.existsSync(file) };
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return { path: file, written: false };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
108
138
|
const liveProvider = {
|
|
109
|
-
snapshot: () => ({
|
|
139
|
+
snapshot: () => ({
|
|
140
|
+
pid: process.pid,
|
|
141
|
+
version: PKG_VERSION,
|
|
142
|
+
at: new Date().toISOString(),
|
|
143
|
+
sessions: board.list(),
|
|
144
|
+
report: reportFile(engines.values().next().value?.memory?.dir ?? lastRun?.dir),
|
|
145
|
+
}),
|
|
110
146
|
// The engine holds no reasoning — it never sees one — so the feed is what the
|
|
111
147
|
// session DID: the action log, which is the same trail a finding's repro uses.
|
|
112
148
|
activity: (session, limit) => feedForSession(engines.get(session)?.memory?.actionLog ?? [], session, limit, redactSecrets),
|
|
@@ -118,7 +154,7 @@ const liveProvider = {
|
|
|
118
154
|
report: () => {
|
|
119
155
|
const eng = (lastWriter && engines.get(lastWriter.session)) ?? engines.values().next().value;
|
|
120
156
|
if (!eng?.memory)
|
|
121
|
-
return null;
|
|
157
|
+
return lastRun ? { markdown: lastRun.markdown, at: lastRun.at } : null;
|
|
122
158
|
const { markdown } = generateReport(eng.memory, eng.oracleLog.all, reportExtras(eng), { write: false });
|
|
123
159
|
return { markdown: redactSecrets(markdown), at: new Date().toISOString() };
|
|
124
160
|
},
|
|
@@ -976,6 +1012,8 @@ server.registerTool("scout_close", {
|
|
|
976
1012
|
for (const e of engines.values())
|
|
977
1013
|
if (e.memory?.dir)
|
|
978
1014
|
dirs.add(e.memory.dir);
|
|
1015
|
+
for (const e of engines.values())
|
|
1016
|
+
keepReport(e);
|
|
979
1017
|
for (const name of engines.keys())
|
|
980
1018
|
live?.dropSession(name);
|
|
981
1019
|
await Promise.allSettled([...engines.values()].map((e) => e.close()));
|
|
@@ -991,6 +1029,7 @@ server.registerTool("scout_close", {
|
|
|
991
1029
|
const eng = engines.get(name);
|
|
992
1030
|
if (!eng)
|
|
993
1031
|
return text(`No live session "${name}".`, name);
|
|
1032
|
+
keepReport(eng);
|
|
994
1033
|
live?.dropSession(name);
|
|
995
1034
|
await eng.close();
|
|
996
1035
|
const saveError = eng.memory?.lastSaveError;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scenescout",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
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",
|