stuntdouble 0.1.2 → 0.2.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/README.md +37 -9
- package/dist/{chunk-VWTYG7G4.js → chunk-BFI6P5DO.js} +65 -25
- package/dist/chunk-BFI6P5DO.js.map +1 -0
- package/dist/chunk-DFXASIGV.js +431 -0
- package/dist/chunk-DFXASIGV.js.map +1 -0
- package/dist/{chunk-JAB46QV7.js → chunk-F3R3QL7V.js} +64 -8
- package/dist/chunk-F3R3QL7V.js.map +1 -0
- package/dist/{mcp-CYPMT37O.js → chunk-FMZTT2MC.js} +72 -1059
- package/dist/chunk-FMZTT2MC.js.map +1 -0
- package/dist/chunk-MSV5OLSY.js +117 -0
- package/dist/chunk-MSV5OLSY.js.map +1 -0
- package/dist/chunk-P3CI4FTE.js +469 -0
- package/dist/chunk-P3CI4FTE.js.map +1 -0
- package/dist/chunk-QDA5FTUH.js +79 -0
- package/dist/chunk-QDA5FTUH.js.map +1 -0
- package/dist/chunk-XHSDRB3X.js +996 -0
- package/dist/chunk-XHSDRB3X.js.map +1 -0
- package/dist/cli.js +180 -68
- package/dist/cli.js.map +1 -1
- package/dist/cloud-DLLNKYND.js +34 -0
- package/dist/credentials-QHESDY4K.js +27 -0
- package/dist/credentials-QHESDY4K.js.map +1 -0
- package/dist/doctor-ZAYOOCPT.js +13 -0
- package/dist/doctor-ZAYOOCPT.js.map +1 -0
- package/dist/files-UBPF7PKW.js +45 -0
- package/dist/files-UBPF7PKW.js.map +1 -0
- package/dist/mcp-HFIAQ6HP.js +133 -0
- package/dist/mcp-HFIAQ6HP.js.map +1 -0
- package/dist/open-browser-CTOGH5HQ.js +17 -0
- package/dist/open-browser-CTOGH5HQ.js.map +1 -0
- package/dist/run-MPIAR3LQ.js +20 -0
- package/dist/run-MPIAR3LQ.js.map +1 -0
- package/dist/runner-E45EJ76B.js +17 -0
- package/dist/runner-E45EJ76B.js.map +1 -0
- package/dist/studio-FBC66BER.js +360 -0
- package/dist/studio-FBC66BER.js.map +1 -0
- package/dist/studio.js +1621 -0
- package/dist/ui-server-C7CFJWRD.js +12 -0
- package/dist/ui-server-C7CFJWRD.js.map +1 -0
- package/dist/ui.html +210 -103
- package/package.json +2 -2
- package/dist/chunk-JAB46QV7.js.map +0 -1
- package/dist/chunk-VWTYG7G4.js.map +0 -1
- package/dist/mcp-CYPMT37O.js.map +0 -1
- package/dist/ui-server-AYIR7DTN.js +0 -10
- /package/dist/{ui-server-AYIR7DTN.js.map → cloud-DLLNKYND.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/ui.html
CHANGED
|
@@ -831,21 +831,6 @@
|
|
|
831
831
|
align-items: center;
|
|
832
832
|
gap: 0.5rem;
|
|
833
833
|
}
|
|
834
|
-
.thumb {
|
|
835
|
-
display: none;
|
|
836
|
-
width: 3rem;
|
|
837
|
-
height: 2rem;
|
|
838
|
-
border: 1px solid var(--border);
|
|
839
|
-
border-radius: var(--radius-md);
|
|
840
|
-
object-fit: cover;
|
|
841
|
-
object-position: top;
|
|
842
|
-
background: var(--muted);
|
|
843
|
-
}
|
|
844
|
-
@media (min-width: 40rem) {
|
|
845
|
-
.thumb {
|
|
846
|
-
display: block;
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
834
|
.chevron {
|
|
850
835
|
color: var(--muted-foreground);
|
|
851
836
|
}
|
|
@@ -1102,15 +1087,16 @@
|
|
|
1102
1087
|
color: var(--muted-foreground);
|
|
1103
1088
|
white-space: nowrap;
|
|
1104
1089
|
}
|
|
1105
|
-
.activity code {
|
|
1106
|
-
font-size: 0.75rem;
|
|
1107
|
-
}
|
|
1108
1090
|
.activity .text {
|
|
1109
1091
|
grid-column: 1 / -1;
|
|
1110
1092
|
white-space: pre-wrap;
|
|
1111
1093
|
overflow-wrap: anywhere;
|
|
1112
1094
|
color: var(--muted-foreground);
|
|
1113
1095
|
}
|
|
1096
|
+
.activity summary {
|
|
1097
|
+
cursor: pointer;
|
|
1098
|
+
white-space: normal;
|
|
1099
|
+
}
|
|
1114
1100
|
@media (min-width: 40rem) {
|
|
1115
1101
|
.activity li {
|
|
1116
1102
|
grid-template-columns: 5.5rem 8rem minmax(0, 1fr);
|
|
@@ -1349,9 +1335,10 @@
|
|
|
1349
1335
|
<footer>
|
|
1350
1336
|
<div class="container">
|
|
1351
1337
|
<p>
|
|
1352
|
-
Local runs use
|
|
1353
|
-
your session is open. The cloud runs on schedules and in parallel,
|
|
1354
|
-
shares reports with your team.
|
|
1338
|
+
Local runs use the account Claude Code is signed in with and a browser on this machine,
|
|
1339
|
+
one at a time while your session is open. The cloud runs on schedules and in parallel,
|
|
1340
|
+
records every run and shares reports with your team.
|
|
1341
|
+
<a href="https://stuntdouble.io/pricing">See plans</a>
|
|
1355
1342
|
</p>
|
|
1356
1343
|
</div>
|
|
1357
1344
|
</footer>
|
|
@@ -1476,8 +1463,9 @@
|
|
|
1476
1463
|
if (style === 'relative') return h('time', { datetime: iso, title: exact }, ago(date));
|
|
1477
1464
|
return h('time', { datetime: iso }, style === 'clock' ? clock.format(date) : exact);
|
|
1478
1465
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1466
|
+
const between = (start, end) => (new Date(end) - new Date(start)) / 1000;
|
|
1467
|
+
function took(seconds) {
|
|
1468
|
+
seconds = Math.round(seconds);
|
|
1481
1469
|
if (!(seconds >= 0)) return null;
|
|
1482
1470
|
if (seconds < 60) return seconds + 's';
|
|
1483
1471
|
const minutes = Math.floor(seconds / 60);
|
|
@@ -1528,6 +1516,7 @@
|
|
|
1528
1516
|
// passed", the same count the finish_run reply gives. Informational and
|
|
1529
1517
|
// skipped checks are left out, as the scorer leaves them out.
|
|
1530
1518
|
const tally = (run) => {
|
|
1519
|
+
if (run.tally) return run.tally;
|
|
1531
1520
|
const checks = (run.checklist_snapshot && run.checklist_snapshot.checks) || [];
|
|
1532
1521
|
const status = Object.fromEntries((run.results || []).map((r) => [r.check, r.status]));
|
|
1533
1522
|
const scored = checks.filter(
|
|
@@ -1545,12 +1534,59 @@
|
|
|
1545
1534
|
return t.total ? t.passed + ' of ' + t.total + ' passed' : '';
|
|
1546
1535
|
};
|
|
1547
1536
|
|
|
1548
|
-
|
|
1537
|
+
// A Studio run names its model; the runs list leaves "self-judged" to the run's page.
|
|
1538
|
+
const runner = (run, full) =>
|
|
1549
1539
|
run.runner.kind === 'cloud'
|
|
1550
1540
|
? { icon: 'server', label: 'Cloud' }
|
|
1551
|
-
: {
|
|
1541
|
+
: {
|
|
1542
|
+
icon: 'monitor',
|
|
1543
|
+
label: run.runner.model
|
|
1544
|
+
? 'Local, ' + run.runner.model.id + (full ? ', self-judged' : '')
|
|
1545
|
+
: 'Local, self-judged',
|
|
1546
|
+
};
|
|
1552
1547
|
const checklistName = (run) =>
|
|
1553
|
-
|
|
1548
|
+
run.checklist_name ||
|
|
1549
|
+
(run.checklist_snapshot && run.checklist_snapshot.name) ||
|
|
1550
|
+
run.checklist;
|
|
1551
|
+
const actorName = (run) => run.actor_name || run.actor;
|
|
1552
|
+
|
|
1553
|
+
// What each tool call did, in words. Page reads fold away: they are the
|
|
1554
|
+
// page's own text, long and full of refs.
|
|
1555
|
+
const ACTIVITY = {
|
|
1556
|
+
start_run: 'Started',
|
|
1557
|
+
navigate: 'Opened a page',
|
|
1558
|
+
read_page: 'Read the page',
|
|
1559
|
+
get_page_text: 'Read the page text',
|
|
1560
|
+
find: 'Looked for',
|
|
1561
|
+
click: 'Clicked',
|
|
1562
|
+
type: 'Typed',
|
|
1563
|
+
form_input: 'Filled a field',
|
|
1564
|
+
key: 'Pressed a key',
|
|
1565
|
+
scroll: 'Scrolled',
|
|
1566
|
+
screenshot: 'Took a screenshot',
|
|
1567
|
+
evaluate_check: 'Judged a check',
|
|
1568
|
+
finish_run: 'Finished',
|
|
1569
|
+
stop: 'Stopped',
|
|
1570
|
+
};
|
|
1571
|
+
const PAGE_READS = new Set(['read_page', 'get_page_text', 'find']);
|
|
1572
|
+
|
|
1573
|
+
// How long the actor worked: a resumed run's idle gap left out. Each
|
|
1574
|
+
// start_run opens a session that ends at its last line.
|
|
1575
|
+
function activeSeconds(run, activity) {
|
|
1576
|
+
let total = 0;
|
|
1577
|
+
let from = null;
|
|
1578
|
+
let last = null;
|
|
1579
|
+
for (const line of activity) {
|
|
1580
|
+
if (line.tool === 'start_run') {
|
|
1581
|
+
if (from && last) total += between(from, last);
|
|
1582
|
+
from = line.at;
|
|
1583
|
+
}
|
|
1584
|
+
last = line.at;
|
|
1585
|
+
}
|
|
1586
|
+
const end = run.finished_at || last;
|
|
1587
|
+
if (from && end) total += between(from, end);
|
|
1588
|
+
return activity.length ? total : between(run.started_at, run.finished_at);
|
|
1589
|
+
}
|
|
1554
1590
|
|
|
1555
1591
|
const RESULT = {
|
|
1556
1592
|
passed: ['Passed', 'success', 'check-circle'],
|
|
@@ -1747,7 +1783,7 @@
|
|
|
1747
1783
|
'span',
|
|
1748
1784
|
{ class: 'fact' },
|
|
1749
1785
|
icon('actor'),
|
|
1750
|
-
h('span', { class: 'truncate' }, entry
|
|
1786
|
+
h('span', { class: 'truncate' }, actorName(entry))
|
|
1751
1787
|
),
|
|
1752
1788
|
h(
|
|
1753
1789
|
'span',
|
|
@@ -1799,6 +1835,9 @@
|
|
|
1799
1835
|
// were and only something that just landed moves.
|
|
1800
1836
|
const toggled = new Map();
|
|
1801
1837
|
const seen = new Map();
|
|
1838
|
+
// Bumped only when the live frame changes, so a redraw reuses the
|
|
1839
|
+
// picture already loaded rather than fetching it again.
|
|
1840
|
+
let frameVersion = Date.now();
|
|
1802
1841
|
|
|
1803
1842
|
function checkRow(run, check, result, opts) {
|
|
1804
1843
|
if (!result) {
|
|
@@ -1853,8 +1892,6 @@
|
|
|
1853
1892
|
})
|
|
1854
1893
|
)
|
|
1855
1894
|
);
|
|
1856
|
-
const thumb =
|
|
1857
|
-
shot && h('img', { class: 'thumb', src: shot, alt: '', loading: 'lazy', hidden: open });
|
|
1858
1895
|
const row = h(
|
|
1859
1896
|
'li',
|
|
1860
1897
|
{ class: 'check' + (open ? ' open' : '') + (opts.arrived ? ' animate-rise' : '') },
|
|
@@ -1871,7 +1908,6 @@
|
|
|
1871
1908
|
const now = detail.hidden;
|
|
1872
1909
|
toggled.set(key, now);
|
|
1873
1910
|
detail.hidden = !now;
|
|
1874
|
-
if (thumb) thumb.hidden = now;
|
|
1875
1911
|
row.classList.toggle('open', now);
|
|
1876
1912
|
row.firstChild.setAttribute('aria-expanded', String(now));
|
|
1877
1913
|
},
|
|
@@ -1886,7 +1922,12 @@
|
|
|
1886
1922
|
h(
|
|
1887
1923
|
'span',
|
|
1888
1924
|
{ class: 'check-side' },
|
|
1889
|
-
|
|
1925
|
+
opts.earlier &&
|
|
1926
|
+
h(
|
|
1927
|
+
'span',
|
|
1928
|
+
{ class: 'badge outline sm', title: 'Judged before the run was resumed' },
|
|
1929
|
+
'Earlier'
|
|
1930
|
+
),
|
|
1890
1931
|
h('span', { class: 'badge sm ' + tone, id: 'label-' + check.key }, label),
|
|
1891
1932
|
icon('chevron-down', 'chevron')
|
|
1892
1933
|
)
|
|
@@ -1926,15 +1967,17 @@
|
|
|
1926
1967
|
? inUnit(score.score, score.unit)
|
|
1927
1968
|
: [String(t.passed), h('span', { class: 'of' }, '/' + t.total)]
|
|
1928
1969
|
),
|
|
1929
|
-
// Unweighted,
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1970
|
+
// Unweighted, there is no score: say how the outcome was decided,
|
|
1971
|
+
// in the words finish_run uses.
|
|
1972
|
+
h(
|
|
1973
|
+
'p',
|
|
1974
|
+
{ class: 'summary-line' },
|
|
1975
|
+
!weighted
|
|
1976
|
+
? 'No score: with no weighted checks, it passes only when every pass/fail check passes'
|
|
1977
|
+
: score.pass_threshold == null
|
|
1935
1978
|
? 'No pass threshold set'
|
|
1936
1979
|
: 'Passes at ' + inUnit(score.pass_threshold, score.unit)
|
|
1937
|
-
|
|
1980
|
+
)
|
|
1938
1981
|
),
|
|
1939
1982
|
h(
|
|
1940
1983
|
'div',
|
|
@@ -2002,8 +2045,10 @@
|
|
|
2002
2045
|
|
|
2003
2046
|
// Stop sits in the live card, beside the tally, as CancelRunButton does.
|
|
2004
2047
|
const pending = data.stop_requested;
|
|
2048
|
+
// A cloud run is polled, not driven from here: nothing to stop.
|
|
2005
2049
|
const stopButton =
|
|
2006
2050
|
running &&
|
|
2051
|
+
run.runner.kind === 'local' &&
|
|
2007
2052
|
h(
|
|
2008
2053
|
'button',
|
|
2009
2054
|
{
|
|
@@ -2044,66 +2089,77 @@
|
|
|
2044
2089
|
|
|
2045
2090
|
const rerun = '/stuntdouble run ' + run.checklist + (resumable ? ' resume' : '');
|
|
2046
2091
|
const cloud = '/stuntdouble cloud ' + run.checklist;
|
|
2047
|
-
const actions =
|
|
2048
|
-
?
|
|
2049
|
-
:
|
|
2050
|
-
?
|
|
2051
|
-
|
|
2052
|
-
'button',
|
|
2053
|
-
{
|
|
2054
|
-
type: 'button',
|
|
2055
|
-
class: 'btn outline',
|
|
2056
|
-
title: rerun,
|
|
2057
|
-
'data-focus-key': 'rerun',
|
|
2058
|
-
onclick: (event) => copy(rerun, event.currentTarget),
|
|
2059
|
-
},
|
|
2060
|
-
icon('copy'),
|
|
2061
|
-
h('span', {}, resumable ? 'Copy resume command' : 'Copy re-run command')
|
|
2062
|
-
),
|
|
2063
|
-
h(
|
|
2064
|
-
'button',
|
|
2065
|
-
{
|
|
2066
|
-
type: 'button',
|
|
2067
|
-
class: 'btn outline',
|
|
2068
|
-
title: cloud,
|
|
2069
|
-
'data-focus-key': 'cloud',
|
|
2070
|
-
onclick: (event) => copy(cloud, event.currentTarget),
|
|
2071
|
-
},
|
|
2072
|
-
icon('copy'),
|
|
2073
|
-
h('span', {}, 'Run in the cloud')
|
|
2074
|
-
),
|
|
2075
|
-
]
|
|
2076
|
-
: webUrl(run.runner.url)
|
|
2092
|
+
const actions = studio.runActions
|
|
2093
|
+
? studio.runActions(run, running, resumable)
|
|
2094
|
+
: running
|
|
2095
|
+
? null
|
|
2096
|
+
: run.runner.kind === 'local'
|
|
2077
2097
|
? [
|
|
2078
2098
|
h(
|
|
2079
|
-
'
|
|
2099
|
+
'button',
|
|
2080
2100
|
{
|
|
2101
|
+
type: 'button',
|
|
2081
2102
|
class: 'btn outline',
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2103
|
+
title: rerun,
|
|
2104
|
+
'data-focus-key': 'rerun',
|
|
2105
|
+
onclick: (event) => copy(rerun, event.currentTarget),
|
|
2085
2106
|
},
|
|
2086
|
-
icon('
|
|
2087
|
-
h('span', {}, '
|
|
2107
|
+
icon('copy'),
|
|
2108
|
+
h('span', {}, resumable ? 'Copy resume command' : 'Copy re-run command')
|
|
2109
|
+
),
|
|
2110
|
+
h(
|
|
2111
|
+
'button',
|
|
2112
|
+
{
|
|
2113
|
+
type: 'button',
|
|
2114
|
+
class: 'btn outline',
|
|
2115
|
+
title: cloud,
|
|
2116
|
+
'data-focus-key': 'cloud',
|
|
2117
|
+
onclick: (event) => copy(cloud, event.currentTarget),
|
|
2118
|
+
},
|
|
2119
|
+
icon('copy'),
|
|
2120
|
+
h('span', {}, 'Copy cloud command')
|
|
2088
2121
|
),
|
|
2089
2122
|
]
|
|
2090
|
-
:
|
|
2123
|
+
: webUrl(run.runner.url)
|
|
2124
|
+
? [
|
|
2125
|
+
h(
|
|
2126
|
+
'a',
|
|
2127
|
+
{
|
|
2128
|
+
class: 'btn outline',
|
|
2129
|
+
href: run.runner.url,
|
|
2130
|
+
target: '_blank',
|
|
2131
|
+
rel: 'noopener noreferrer',
|
|
2132
|
+
},
|
|
2133
|
+
icon('external-link'),
|
|
2134
|
+
h('span', {}, 'Open this run in Stunt Double')
|
|
2135
|
+
),
|
|
2136
|
+
]
|
|
2137
|
+
: null;
|
|
2091
2138
|
|
|
2092
|
-
const
|
|
2139
|
+
const starts = data.activity.filter((line) => line.tool === 'start_run');
|
|
2140
|
+
const sessions = starts.length;
|
|
2141
|
+
const duration =
|
|
2142
|
+
run.finished_at &&
|
|
2143
|
+
took(activeSeconds(run, data.activity)) +
|
|
2144
|
+
(sessions > 1 ? ' over ' + sessions + ' sessions' : '');
|
|
2145
|
+
// Resumed: the verdicts from before the last start_run stood.
|
|
2146
|
+
const resumedAt = sessions > 1 ? new Date(starts[sessions - 1].at) : null;
|
|
2147
|
+
const earlier = (result) => resumedAt && result.at && new Date(result.at) < resumedAt;
|
|
2093
2148
|
const header = pageHeader({
|
|
2094
2149
|
back: { href: '/', label: 'Runs', trail: run.id },
|
|
2095
2150
|
title: checklistName(run),
|
|
2096
|
-
|
|
2151
|
+
// A finished run's verdict is the score card's stamp: once is enough.
|
|
2152
|
+
badge: run.score && runState(run).outcome ? null : stateBadge(run, 'lg'),
|
|
2097
2153
|
chips: [
|
|
2098
|
-
{ icon: runner(run).icon, label: runner(run).label },
|
|
2099
|
-
{ icon: 'actor', label: run
|
|
2154
|
+
{ icon: runner(run).icon, label: runner(run, true).label },
|
|
2155
|
+
{ icon: 'actor', label: actorName(run), title: 'Actor' },
|
|
2100
2156
|
{ icon: 'globe', label: run.url, href: webUrl(run.url), title: 'Target' },
|
|
2101
2157
|
{ icon: 'calendar', label: time(run.started_at), title: 'Started' },
|
|
2102
2158
|
duration && {
|
|
2103
2159
|
icon: 'timer',
|
|
2104
2160
|
label: duration,
|
|
2105
2161
|
mono: true,
|
|
2106
|
-
title: 'How long the
|
|
2162
|
+
title: 'How long the actor worked, idle time between sessions left out',
|
|
2107
2163
|
},
|
|
2108
2164
|
].filter(Boolean),
|
|
2109
2165
|
actions,
|
|
@@ -2168,12 +2224,13 @@
|
|
|
2168
2224
|
style: 'width:' + (checks.length ? (judged / checks.length) * 100 : 0) + '%',
|
|
2169
2225
|
})
|
|
2170
2226
|
),
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2227
|
+
run.runner.kind === 'local' &&
|
|
2228
|
+
h('img', {
|
|
2229
|
+
class: 'frame',
|
|
2230
|
+
id: 'frame',
|
|
2231
|
+
src: fileUrl('.live/latest.png') + '?v=' + frameVersion,
|
|
2232
|
+
alt: 'The browser now',
|
|
2233
|
+
})
|
|
2177
2234
|
);
|
|
2178
2235
|
|
|
2179
2236
|
// What went wrong first, since that is what somebody opens a report to
|
|
@@ -2210,6 +2267,7 @@
|
|
|
2210
2267
|
'div',
|
|
2211
2268
|
{ class: 'stack' },
|
|
2212
2269
|
live,
|
|
2270
|
+
studio.runExtras && studio.runExtras(run),
|
|
2213
2271
|
notice,
|
|
2214
2272
|
summaryCard(run, fresh),
|
|
2215
2273
|
groups.map((group) =>
|
|
@@ -2230,6 +2288,7 @@
|
|
|
2230
2288
|
running,
|
|
2231
2289
|
openByDefault: group.openByDefault,
|
|
2232
2290
|
arrived: results.has(check.key) && arrived(check.key),
|
|
2291
|
+
earlier: results.has(check.key) && earlier(results.get(check.key)),
|
|
2233
2292
|
})
|
|
2234
2293
|
)
|
|
2235
2294
|
)
|
|
@@ -2253,8 +2312,21 @@
|
|
|
2253
2312
|
'li',
|
|
2254
2313
|
{},
|
|
2255
2314
|
time(line.at, 'clock'),
|
|
2256
|
-
h('
|
|
2257
|
-
|
|
2315
|
+
h('span', { title: line.tool }, ACTIVITY[line.tool] || line.tool),
|
|
2316
|
+
PAGE_READS.has(line.tool)
|
|
2317
|
+
? h(
|
|
2318
|
+
'details',
|
|
2319
|
+
{ class: 'text' },
|
|
2320
|
+
h(
|
|
2321
|
+
'summary',
|
|
2322
|
+
{},
|
|
2323
|
+
line.tool === 'find' && line.input && line.input.query
|
|
2324
|
+
? '"' + line.input.query + '": show matches'
|
|
2325
|
+
: 'Show page text'
|
|
2326
|
+
),
|
|
2327
|
+
line.text
|
|
2328
|
+
)
|
|
2329
|
+
: h('span', { class: 'text' }, line.text)
|
|
2258
2330
|
)
|
|
2259
2331
|
)
|
|
2260
2332
|
)
|
|
@@ -2382,28 +2454,59 @@
|
|
|
2382
2454
|
];
|
|
2383
2455
|
}
|
|
2384
2456
|
|
|
2457
|
+
// What Stunt Double Studio's script (studio.js) builds on. The viewer on
|
|
2458
|
+
// its own leaves it empty: inside Claude Code it reads and stops, no more.
|
|
2459
|
+
const studio = (window.stuntdouble = {
|
|
2460
|
+
routes: [],
|
|
2461
|
+
runActions: null,
|
|
2462
|
+
runExtras: null,
|
|
2463
|
+
h,
|
|
2464
|
+
icon,
|
|
2465
|
+
ICONS,
|
|
2466
|
+
pageHeader,
|
|
2467
|
+
emptyState,
|
|
2468
|
+
getJson,
|
|
2469
|
+
announce,
|
|
2470
|
+
copy,
|
|
2471
|
+
time,
|
|
2472
|
+
webUrl,
|
|
2473
|
+
checkMeta,
|
|
2474
|
+
renderRuns,
|
|
2475
|
+
render: (enter) => render(enter),
|
|
2476
|
+
});
|
|
2477
|
+
|
|
2385
2478
|
let rendering = Promise.resolve();
|
|
2386
|
-
|
|
2479
|
+
// `live`: a redraw for a file change. A view holding a form the reader
|
|
2480
|
+
// is filling in (`keep`) is left alone.
|
|
2481
|
+
function render(enter, live) {
|
|
2387
2482
|
rendering = rendering.then(async () => {
|
|
2388
2483
|
const path = location.pathname;
|
|
2484
|
+
const kept = studio.routes.find((r) => r.match(path));
|
|
2485
|
+
if (live && kept && kept.keep) return;
|
|
2389
2486
|
for (const link of document.querySelectorAll('.bar nav a')) {
|
|
2390
|
-
const
|
|
2487
|
+
const href = link.getAttribute('href');
|
|
2488
|
+
const current =
|
|
2489
|
+
href === (path.startsWith('/runs/') ? '/' : path) ||
|
|
2490
|
+
(href !== '/' && path.startsWith(href + '/'));
|
|
2391
2491
|
if (current) link.setAttribute('aria-current', 'page');
|
|
2392
2492
|
else link.removeAttribute('aria-current');
|
|
2393
2493
|
}
|
|
2394
2494
|
let view;
|
|
2495
|
+
const route = studio.routes.find((r) => r.match(path));
|
|
2395
2496
|
try {
|
|
2396
|
-
view =
|
|
2397
|
-
? await
|
|
2398
|
-
: path
|
|
2399
|
-
? await
|
|
2400
|
-
:
|
|
2497
|
+
view = route
|
|
2498
|
+
? await route.render(path)
|
|
2499
|
+
: path.startsWith('/runs/')
|
|
2500
|
+
? await renderRun(decodeURIComponent(path.slice('/runs/'.length)))
|
|
2501
|
+
: path === '/definitions'
|
|
2502
|
+
? await renderDefinitions()
|
|
2503
|
+
: await renderRuns();
|
|
2401
2504
|
} catch (err) {
|
|
2402
2505
|
view = [
|
|
2403
2506
|
h('div', { class: 'notice destructive' }, icon('octagon-x'), h('p', {}, err.message)),
|
|
2404
2507
|
];
|
|
2405
2508
|
}
|
|
2406
|
-
app.classList.toggle('narrow', path === '/definitions');
|
|
2509
|
+
app.classList.toggle('narrow', path === '/definitions' || Boolean(route && route.narrow));
|
|
2407
2510
|
// A live redraw replaces the view: keep keyboard focus where it was.
|
|
2408
2511
|
const focused = document.activeElement?.closest?.('[data-focus-key]');
|
|
2409
2512
|
const focusKey = focused && app.contains(focused) && focused.dataset.focusKey;
|
|
@@ -2433,24 +2536,28 @@
|
|
|
2433
2536
|
});
|
|
2434
2537
|
window.addEventListener('popstate', () => render(false));
|
|
2435
2538
|
|
|
2436
|
-
// Live updates: a new frame swaps the picture, anything else redraws the
|
|
2539
|
+
// Live updates: a new frame swaps the picture, anything else redraws the
|
|
2540
|
+
// page. The server already coalesces changes, so a redraw waits only long
|
|
2541
|
+
// enough to fold a burst into one.
|
|
2437
2542
|
let queued = false;
|
|
2438
2543
|
new EventSource('/api/events').onmessage = (event) => {
|
|
2439
2544
|
const { paths } = JSON.parse(event.data);
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
frame
|
|
2545
|
+
if (paths.includes('.live/latest.png')) {
|
|
2546
|
+
frameVersion++;
|
|
2547
|
+
const frame = document.getElementById('frame');
|
|
2548
|
+
if (frame) frame.src = fileUrl('.live/latest.png') + '?v=' + frameVersion;
|
|
2443
2549
|
}
|
|
2444
2550
|
if (paths.every((p) => p.startsWith('.live/')) || queued) return;
|
|
2445
2551
|
queued = true;
|
|
2446
2552
|
setTimeout(() => {
|
|
2447
2553
|
queued = false;
|
|
2448
|
-
render(false);
|
|
2449
|
-
},
|
|
2554
|
+
render(false, true);
|
|
2555
|
+
}, 50);
|
|
2450
2556
|
};
|
|
2451
2557
|
// Relative times on the runs list stay current.
|
|
2452
2558
|
setInterval(() => location.pathname === '/' && render(false), 60_000);
|
|
2453
|
-
|
|
2559
|
+
// After every script on the page: the Studio's registers its views first.
|
|
2560
|
+
document.addEventListener('DOMContentLoaded', () => render(false));
|
|
2454
2561
|
</script>
|
|
2455
2562
|
</body>
|
|
2456
2563
|
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stuntdouble",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Stunt Double Local: run Stunt Double checklists in a local Chrome from your own coding agent session, with the runs kept as files and a local viewer",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"stunt-double",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"zod": "^4.6.7"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@
|
|
46
|
+
"@stuntkit/browser-toolset": "workspace:*"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
49
49
|
"node": ">=22.18"
|