@stage5/lumine 0.3.46 → 0.3.48
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/lib/admin-owner-trace.js +55 -0
- package/lib/admin.js +6 -0
- package/lib/build-review.js +110 -18
- package/lib/commands.js +1 -1
- package/lib/constants.js +1 -1
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +2 -2
package/lib/admin-owner-trace.js
CHANGED
|
@@ -13,6 +13,8 @@ export const OWNER_TRACE_PERF_TYPES = [
|
|
|
13
13
|
"slow-request",
|
|
14
14
|
"stall",
|
|
15
15
|
"page-load",
|
|
16
|
+
"scroll-perf",
|
|
17
|
+
"scroll-frame",
|
|
16
18
|
];
|
|
17
19
|
|
|
18
20
|
const MAX_LIMIT = 2000;
|
|
@@ -134,6 +136,10 @@ export function describeOwnerTraceEvent(event) {
|
|
|
134
136
|
].filter(Boolean);
|
|
135
137
|
return `${data.m || "?"} ${data.api || "?"} → ${data.status ?? "?"} in ${data.ms}ms${extra.length ? ` (${extra.join(", ")})` : ""}`;
|
|
136
138
|
}
|
|
139
|
+
case "scroll-perf":
|
|
140
|
+
return describeScrollPerf(data);
|
|
141
|
+
case "scroll-frame":
|
|
142
|
+
return describeScrollFrame(data);
|
|
137
143
|
case "stall":
|
|
138
144
|
return `main thread blocked ${data.ms}ms${data.n > 1 ? ` (${data.n} stalls, ${data.total}ms blocked over ${data.span}ms)` : ""}`;
|
|
139
145
|
case "page-load": {
|
|
@@ -154,6 +160,55 @@ export function describeOwnerTraceEvent(event) {
|
|
|
154
160
|
}
|
|
155
161
|
}
|
|
156
162
|
|
|
163
|
+
// One Home/page scroll burst: frame pacing plus what was on screen.
|
|
164
|
+
function describeScrollPerf(data) {
|
|
165
|
+
const cards = (data.context?.cards || [])
|
|
166
|
+
.map((card) => card.key)
|
|
167
|
+
.filter(Boolean);
|
|
168
|
+
const source = data.frameSource
|
|
169
|
+
? `${data.frameSource.source === "loaf" ? `LoAF ${data.frameSource.loafFrames ?? 0} long frame(s)` : "no LoAF"} · card renders ${data.frameSource.cardRenders} mounts ${data.frameSource.cardMounts} · img loads ${data.frameSource.imgLoads}`
|
|
170
|
+
: "";
|
|
171
|
+
return [
|
|
172
|
+
`${data.frames ?? 0} frames over ${data.spanMs ?? 0}ms`,
|
|
173
|
+
`mean ${data.meanFrameMs ?? "?"}ms max ${data.maxFrameMs ?? "?"}ms`,
|
|
174
|
+
`>50ms ${data.over50 ?? 0} >100ms ${data.over100 ?? 0}`,
|
|
175
|
+
cards.length ? `cards ${cards.join(",")}` : "",
|
|
176
|
+
source,
|
|
177
|
+
data.reason ? `(${data.reason})` : "",
|
|
178
|
+
]
|
|
179
|
+
.filter(Boolean)
|
|
180
|
+
.join(" · ");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// One slow frame of a scroll burst: Long Animation Frame scripts (Chrome) or
|
|
184
|
+
// the feed card types that rendered in it (every browser).
|
|
185
|
+
function describeScrollFrame(data) {
|
|
186
|
+
const counts = (label, map) => {
|
|
187
|
+
const entries = Object.entries(map || {});
|
|
188
|
+
return entries.length
|
|
189
|
+
? `${label} ${entries.map(([type, n]) => `${type}×${n}`).join(",")}`
|
|
190
|
+
: "";
|
|
191
|
+
};
|
|
192
|
+
const loaf = data.loaf;
|
|
193
|
+
const head = loaf
|
|
194
|
+
? `slow frame ${loaf.ms}ms (LoAF, blocking ${loaf.blockingMs ?? "?"}ms${loaf.styleLayoutShare != null ? `, style/layout ${Math.round(loaf.styleLayoutShare * 100)}%` : ""}${data.gapMs != null ? `, rAF gap ${data.gapMs}ms` : ""})`
|
|
195
|
+
: `slow frame ${data.gapMs}ms (rAF gap, no LoAF)`;
|
|
196
|
+
const scripts = (loaf?.scripts || []).map(
|
|
197
|
+
(script) =>
|
|
198
|
+
`${script.ms}ms ${script.src || "?"}${script.fn ? ` ${script.fn}` : ""} [${script.type || "?"}${script.invoker ? ` ${script.invoker}` : ""}]${script.forcedLayoutMs ? ` forced layout ${script.forcedLayoutMs}ms` : ""}`,
|
|
199
|
+
);
|
|
200
|
+
return [
|
|
201
|
+
head,
|
|
202
|
+
data.offsetMs != null ? `+${data.offsetMs}ms` : "",
|
|
203
|
+
scripts.length ? `scripts: ${scripts.join("; ")}` : "",
|
|
204
|
+
counts("rendered", data.renders),
|
|
205
|
+
counts("mounted", data.mounts),
|
|
206
|
+
data.imgLoads ? `${data.imgLoads} img load(s)` : "",
|
|
207
|
+
]
|
|
208
|
+
.filter(Boolean)
|
|
209
|
+
.join(" · ");
|
|
210
|
+
}
|
|
211
|
+
|
|
157
212
|
// "tap→page 4210ms · PUSH via nav · url +90 · gate +4180 (probe timeout,
|
|
158
213
|
// waited 4100ms) · commit +4210 · ready +4600": offsets are ms from the tap.
|
|
159
214
|
function describeNavTiming(data) {
|
package/lib/admin.js
CHANGED
|
@@ -7622,6 +7622,12 @@ function printAdminResult({ operation, result }) {
|
|
|
7622
7622
|
`Confirmed Build #${data.review.buildId} runtime at published artifact #${data.review.publishedArtifactVersionId}.`,
|
|
7623
7623
|
);
|
|
7624
7624
|
console.log(`Screenshot: ${data.screenshotPath}`);
|
|
7625
|
+
const teardown = data.review.browser?.teardownWarning;
|
|
7626
|
+
if (teardown) {
|
|
7627
|
+
console.log(
|
|
7628
|
+
`Teardown warning: Chrome ended with ${teardown.signal ? `signal ${teardown.signal}` : `code ${teardown.exitCode}`} (${teardown.signature}) after the evidence was captured and Browser.close was sent; the review is still confirmed.`,
|
|
7629
|
+
);
|
|
7630
|
+
}
|
|
7625
7631
|
if (data.review.interaction) {
|
|
7626
7632
|
console.log(
|
|
7627
7633
|
`Interaction script: ${data.review.interaction.stepsCompleted}/${data.review.interaction.stepsPlanned} step(s) ${data.review.interaction.status}.`,
|
package/lib/build-review.js
CHANGED
|
@@ -745,6 +745,9 @@ async function runChromeReview({
|
|
|
745
745
|
});
|
|
746
746
|
let stdout = "";
|
|
747
747
|
let stderr = "";
|
|
748
|
+
// Only what Chrome printed after our own Browser.close, so a teardown
|
|
749
|
+
// signature is never matched against a message from before the close.
|
|
750
|
+
let stderrAfterClose = null;
|
|
748
751
|
const consoleLines = [];
|
|
749
752
|
const append = (current, chunk) => `${current}${chunk}`.slice(-2_000_000);
|
|
750
753
|
child.stdout.on("data", (chunk) => {
|
|
@@ -752,6 +755,9 @@ async function runChromeReview({
|
|
|
752
755
|
});
|
|
753
756
|
child.stderr.on("data", (chunk) => {
|
|
754
757
|
stderr = append(stderr, chunk);
|
|
758
|
+
if (stderrAfterClose !== null) {
|
|
759
|
+
stderrAfterClose = `${stderrAfterClose}${chunk}`.slice(-200_000);
|
|
760
|
+
}
|
|
755
761
|
});
|
|
756
762
|
const closed = new Promise((resolve, reject) => {
|
|
757
763
|
child.once("error", reject);
|
|
@@ -908,22 +914,36 @@ async function runChromeReview({
|
|
|
908
914
|
}
|
|
909
915
|
}
|
|
910
916
|
}
|
|
917
|
+
// Everything the receipt relies on is in hand before the close; a
|
|
918
|
+
// nonzero exit can only be read as teardown when that is true and Chrome
|
|
919
|
+
// was still running when we asked it to close.
|
|
920
|
+
const evidenceCompleteBeforeClose =
|
|
921
|
+
Boolean(screenshot.data) &&
|
|
922
|
+
isRuntimeReady(readiness) &&
|
|
923
|
+
(!interaction || interactionResult?.status === "completed");
|
|
924
|
+
const exitedBeforeClose =
|
|
925
|
+
child.exitCode !== null || child.signalCode !== null;
|
|
926
|
+
const closeRequestedAt = Date.now();
|
|
927
|
+
stderrAfterClose = "";
|
|
911
928
|
await cdp.send("Browser.close").catch(() => {});
|
|
912
929
|
let forcedTermination = false;
|
|
913
930
|
const closeResult = await Promise.race([
|
|
914
931
|
closed,
|
|
915
932
|
delay(5_000).then(() => {
|
|
916
933
|
forcedTermination = true;
|
|
917
|
-
child
|
|
918
|
-
return closed;
|
|
934
|
+
return stopChrome(child, closed);
|
|
919
935
|
}),
|
|
920
936
|
]);
|
|
921
937
|
return {
|
|
922
938
|
...closeResult,
|
|
923
939
|
closeRequested: true,
|
|
924
940
|
forcedTermination,
|
|
941
|
+
evidenceCompleteBeforeClose,
|
|
942
|
+
exitedBeforeClose,
|
|
943
|
+
closeDurationMs: Date.now() - closeRequestedAt,
|
|
925
944
|
stdout,
|
|
926
945
|
stderr,
|
|
946
|
+
stderrAfterClose,
|
|
927
947
|
consoleLines: consoleLines.slice(-MAX_CAPTURED_LOG_LINES),
|
|
928
948
|
runtimeReadiness: {
|
|
929
949
|
...(readiness || {}),
|
|
@@ -934,24 +954,85 @@ async function runChromeReview({
|
|
|
934
954
|
};
|
|
935
955
|
} finally {
|
|
936
956
|
clearTimeout(timer);
|
|
937
|
-
if (child.exitCode === null && child.signalCode === null)
|
|
957
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
958
|
+
await stopChrome(child, closed).catch(() => {});
|
|
938
959
|
}
|
|
939
960
|
}
|
|
940
961
|
|
|
962
|
+
// SIGTERM, then SIGKILL if Chrome is still there after 3 s: a browser wedged
|
|
963
|
+
// in teardown (the case this review must tolerate) can ignore SIGTERM, and
|
|
964
|
+
// the review must never hang or leave Chrome running.
|
|
965
|
+
async function stopChrome(child, closed) {
|
|
966
|
+
child.kill("SIGTERM");
|
|
967
|
+
const ended = await Promise.race([closed, delay(3_000).then(() => null)]);
|
|
968
|
+
if (ended) return ended;
|
|
969
|
+
child.kill("SIGKILL");
|
|
970
|
+
return closed;
|
|
971
|
+
}
|
|
972
|
+
|
|
941
973
|
const CHROME_KILLED_AFTER_CLOSE_CODE = 2;
|
|
942
974
|
|
|
943
|
-
// Chrome
|
|
944
|
-
//
|
|
945
|
-
//
|
|
946
|
-
//
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
975
|
+
// Lines Chrome prints when its own shutdown, not the page, went wrong.
|
|
976
|
+
// "Teardown watchdog expired" is the network-service watchdog that kills a
|
|
977
|
+
// browser whose services do not stop in time after Browser.close.
|
|
978
|
+
// Anchored to the start of the line: Chrome prints the bare message (seen
|
|
979
|
+
// 10-19) or behind its own ERROR/FATAL prefix. A page console line is
|
|
980
|
+
// "[pid:tid:...:INFO:CONSOLE:n] \"...\"", so page text cannot match.
|
|
981
|
+
const CHROME_TEARDOWN_SIGNATURES = [
|
|
982
|
+
{
|
|
983
|
+
id: "teardown_watchdog_expired",
|
|
984
|
+
pattern:
|
|
985
|
+
/^(?:\[[^\]]*:(?:ERROR|FATAL):[^\]]*\]\s*)?teardown watchdog expired/i,
|
|
986
|
+
},
|
|
987
|
+
];
|
|
988
|
+
|
|
989
|
+
function chromeTeardownSignature(stderrAfterClose) {
|
|
990
|
+
for (const line of String(stderrAfterClose || "").split(/\r?\n/)) {
|
|
991
|
+
const match = CHROME_TEARDOWN_SIGNATURES.find(({ pattern }) =>
|
|
992
|
+
pattern.test(line),
|
|
993
|
+
);
|
|
994
|
+
if (match) return { id: match.id, line: line.trim().slice(0, 400) };
|
|
995
|
+
}
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Capture validity and process cleanup are separate questions. Exit 0 is
|
|
1000
|
+
// clean. A nonzero exit (or our forced stop) is accepted as a teardown
|
|
1001
|
+
// warning only when all page evidence was complete before our own
|
|
1002
|
+
// Browser.close, Chrome was still running when we sent it, and the exit is
|
|
1003
|
+
// a known shutdown outcome: exit 2 (RESULT_CODE_KILLED) on its own, or any
|
|
1004
|
+
// exit with a teardown signature printed after the close. Anything else,
|
|
1005
|
+
// including every exit before the evidence was complete, fails the review.
|
|
1006
|
+
export function classifyReviewBrowserExit(browser) {
|
|
1007
|
+
if (browser?.code === 0 && !browser?.signal) {
|
|
1008
|
+
return { accepted: true, teardownWarning: null };
|
|
1009
|
+
}
|
|
1010
|
+
const afterOwnClose =
|
|
1011
|
+
browser?.closeRequested === true &&
|
|
1012
|
+
browser?.exitedBeforeClose === false &&
|
|
1013
|
+
browser?.evidenceCompleteBeforeClose === true;
|
|
1014
|
+
if (!afterOwnClose) return { accepted: false, teardownWarning: null };
|
|
1015
|
+
const signature = chromeTeardownSignature(browser.stderrAfterClose);
|
|
1016
|
+
const killedAfterClose =
|
|
1017
|
+
browser.code === CHROME_KILLED_AFTER_CLOSE_CODE &&
|
|
1018
|
+
!browser.signal &&
|
|
1019
|
+
browser.forcedTermination === false;
|
|
1020
|
+
if (!signature && !killedAfterClose) {
|
|
1021
|
+
return { accepted: false, teardownWarning: null };
|
|
1022
|
+
}
|
|
1023
|
+
return {
|
|
1024
|
+
accepted: true,
|
|
1025
|
+
teardownWarning: {
|
|
1026
|
+
exitCode: browser.code,
|
|
1027
|
+
signal: browser.signal || null,
|
|
1028
|
+
forcedTermination: browser.forcedTermination === true,
|
|
1029
|
+
closeDurationMs: Number.isFinite(browser.closeDurationMs)
|
|
1030
|
+
? browser.closeDurationMs
|
|
1031
|
+
: null,
|
|
1032
|
+
signature: signature?.id || "exit_2_after_close",
|
|
1033
|
+
stderrLine: signature?.line || null,
|
|
1034
|
+
},
|
|
1035
|
+
};
|
|
955
1036
|
}
|
|
956
1037
|
|
|
957
1038
|
// The last few Chrome stderr lines, for diagnosing a nonzero exit.
|
|
@@ -1032,7 +1113,8 @@ export async function runManagedBuildReview({ options, authToken, buildId }) {
|
|
|
1032
1113
|
const interactionOutcome = browser.interaction || null;
|
|
1033
1114
|
const interactionCompleted =
|
|
1034
1115
|
!interaction || interactionOutcome?.status === "completed";
|
|
1035
|
-
const
|
|
1116
|
+
const browserExit = classifyReviewBrowserExit(browser);
|
|
1117
|
+
const browserExitedCleanly = browserExit.accepted;
|
|
1036
1118
|
const confirmed =
|
|
1037
1119
|
browserExitedCleanly &&
|
|
1038
1120
|
screenshotBytes > 0 &&
|
|
@@ -1057,7 +1139,15 @@ export async function runManagedBuildReview({ options, authToken, buildId }) {
|
|
|
1057
1139
|
signal: browser.signal,
|
|
1058
1140
|
closeRequested: browser.closeRequested === true,
|
|
1059
1141
|
forcedTermination: browser.forcedTermination === true,
|
|
1060
|
-
exitedCleanly:
|
|
1142
|
+
exitedCleanly: browser.code === 0 && !browser.signal,
|
|
1143
|
+
evidenceCompleteBeforeClose: browser.evidenceCompleteBeforeClose === true,
|
|
1144
|
+
exitedBeforeClose: browser.exitedBeforeClose === true,
|
|
1145
|
+
closeDurationMs: Number.isFinite(browser.closeDurationMs)
|
|
1146
|
+
? browser.closeDurationMs
|
|
1147
|
+
: null,
|
|
1148
|
+
// Set when the page evidence is valid but Chrome's own shutdown ended
|
|
1149
|
+
// nonzero; the review stays confirmed and this says what happened.
|
|
1150
|
+
teardownWarning: browserExit.teardownWarning,
|
|
1061
1151
|
stderrTail: browser.code === 0 ? null : chromeStderrTail(browser.stderr),
|
|
1062
1152
|
runtimeReadiness: browser.runtimeReadiness,
|
|
1063
1153
|
},
|
|
@@ -1097,7 +1187,9 @@ export async function runManagedBuildReview({ options, authToken, buildId }) {
|
|
|
1097
1187
|
screenshotBytes > 0 &&
|
|
1098
1188
|
browser.runtimeReadiness?.ready === true &&
|
|
1099
1189
|
interactionCompleted
|
|
1100
|
-
?
|
|
1190
|
+
? browser.exitedBeforeClose === true
|
|
1191
|
+
? `Chrome exited with ${browser.signal ? `signal ${browser.signal}` : `code ${browser.code}`} before the CLI asked it to close, so the run is not trusted. The screenshots are in the receipt; see browser.stderrTail and review again.`
|
|
1192
|
+
: `Chrome exited with ${browser.signal ? `signal ${browser.signal}` : `code ${browser.code}`}${browser.forcedTermination ? " after the CLI had to stop it" : ""} while closing, with no known teardown signature. The app loaded and the screenshots are in the receipt, but the receipt is not confirmed; see browser.stderrTail and review again.`
|
|
1101
1193
|
: interactionFailure
|
|
1102
1194
|
? `Interaction step ${interactionOutcome?.failedStep?.index ?? "?"} (${interactionOutcome?.failedStep?.kind ?? "?"}) failed: ${interactionOutcome?.failedStep?.error ?? "unknown"} The receipt lists the ${interactionOutcome?.stepsCompleted ?? 0} completed step(s) and their screenshots; fix the script and review again.`
|
|
1103
1195
|
: "The isolated Build runtime did not become visibly reviewable before the observation window ended.",
|
package/lib/commands.js
CHANGED
|
@@ -3311,7 +3311,7 @@ export function printHelp() {
|
|
|
3311
3311
|
lumine admin reward-bank coverage <buildId> [--max-step 3] [--json]
|
|
3312
3312
|
lumine admin reward-bank seed <buildId> [--max-step 3] [--identity zero|ciel|auto] [--json] (writes ONE missing seed question, lowest step first, billed to Zero/Ciel's AI Energy; inside a daily run or on its own, owner only)
|
|
3313
3313
|
lumine admin reward-bank report <buildId> [--days 1] [--json] (question-bank telemetry: generation outcomes, learners, payouts, ladder, questions to review)
|
|
3314
|
-
lumine admin owner-trace [--since 2h|30m|3d|<iso>] [--type route,feed-open,scroll-anchor,content-scroll,error,rejection,session,nav-timing,slow-request,stall,page-load] [--perf] [--path <substring>] [--limit 300] [--json] (what the website did on Mikey's own devices: routes with PUSH/POP/REPLACE, Home feed card opens, scroll-anchor restores, content scroll positions, window errors, and performance: tap-to-page timing, API requests over 1 s, main-thread stalls over 200 ms, page load; --perf shows only those; kept 30 days)
|
|
3314
|
+
lumine admin owner-trace [--since 2h|30m|3d|<iso>] [--type route,feed-open,scroll-anchor,content-scroll,error,rejection,session,nav-timing,slow-request,stall,page-load,scroll-perf,scroll-frame] [--perf] [--path <substring>] [--limit 300] [--json] (what the website did on Mikey's own devices: routes with PUSH/POP/REPLACE, Home feed card opens, scroll-anchor restores, content scroll positions, window errors, and performance: tap-to-page timing, API requests over 1 s, main-thread stalls over 200 ms, page load, scroll frame pacing and slow-frame attribution (Long Animation Frame scripts on Chrome, feed card renders on Safari); --perf shows only those; kept 30 days)
|
|
3315
3315
|
lumine admin roles [--min-level 1] [--limit 50] [--json] (every user type with its management level and who holds it; admin pages need level 3, achievement points never pass level 2)
|
|
3316
3316
|
lumine admin comment draft <target> [--type subject|comment|build|aiStory|dailyReflection] [--file <comment.md>] [--review-receipt <review.json>|--reviewed-version <id> --reviewed-via runtime|code] [--review-context <context.json>] [--identity zero|ciel|auto] [--json]
|
|
3317
3317
|
lumine admin comment reply comment:<id> [--file <reply.md>] [--reviewed-version <id> --reviewed-via runtime|code] [--review-context <context.json>] [--identity zero|ciel|auto] [--json]
|
package/lib/constants.js
CHANGED
|
@@ -350,7 +350,7 @@ lumine save --summary "Describe the change"
|
|
|
350
350
|
- Use local project files with relative or root-local imports only. Do not add package imports, CDN scripts, external network calls, or app-local /api/* routes.
|
|
351
351
|
- Build apps run in sandboxed iframes without allow-forms. Do not use <form> elements, native form submission, requestSubmit(), or browser form navigation. Build input flows with JavaScript-handled inputs and buttons instead.
|
|
352
352
|
${LUMINE_MOBILE_SELECTION_GUIDANCE}
|
|
353
|
-
- CAUTION: the preview runtime AUTO-DETECTS "game apps" — any <canvas> in the body (
|
|
353
|
+
- CAUTION: the preview runtime AUTO-DETECTS "game apps" — any <canvas> in the body except a small one on screen (both sides at most 160 CSS px: a visualizer, a sparkle, an icon) switches the app to viewport-app mode: html/body get overflow:hidden !important and body becomes a centering flexbox, so tall document-flow pages clip and stop scrolling. A hidden or not-yet-sized canvas counts too, and a page that sets data-twinkle-game keeps this layout with any canvas. Document-style apps with a larger canvas (a chart, a drawing pad, a background effect) set data-twinkle-document on <html> or <body>, or call Twinkle.preview.subscribe (or getLayout/reserveInsets) early at boot and then pad by layout.safeInsets and scroll within layout.viewport.height.
|
|
354
354
|
- For canvas, WebGL, Three.js, fullscreen, or game builds, use Twinkle.preview for layout. Do not size roots from 100vh, 100vw, 100dvh, 100dvw, window.innerWidth, window.innerHeight, visualViewport, or document viewport dimensions.
|
|
355
355
|
${LUMINE_THREE_VENDOR_GUIDANCE}
|
|
356
356
|
${LUMINE_MATH_VENDOR_GUIDANCE}
|
package/package.json
CHANGED
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Version: 1.68.0
|
|
4
4
|
Updated: 2026-10-16
|
|
5
|
-
Generated: 2026-10-
|
|
5
|
+
Generated: 2026-10-19T05:26:13.264Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -25,7 +25,7 @@ Generated: 2026-10-18T22:57:11.632Z
|
|
|
25
25
|
- Twinkle.ai.chat history entries must use { role, content }; map local message.text fields to content before passing history.
|
|
26
26
|
- Live web search is enabled by default for Twinkle.ai.chat and for Medium/High Twinkle.ai.generateObject and Twinkle.characters.chat requests. App authors can pass webSearch: false to disable it for their app. Search uses the provider's live web-search tool and is included in AI Energy usage; structured and character Lite Mode remains tool-free.
|
|
27
27
|
- Mobile long-press must not select game UI or open browser Copy/Look Up/image menus. The SDK provides no-selection/callout defaults for standard buttons, button-like ARIA controls, and canvases. Mark the entire custom gameplay wrapper data-twinkle-no-select, including HUD, labels, scores, menus, controls, and empty play space; also style it with user-select: none, -webkit-user-select: none, and -webkit-touch-callout: none for local previews. Protecting only one button or the canvas is insufficient. This behavior is independent of Twinkle.preview layout mode.
|
|
28
|
-
- Played game pages block text selection, the long-press menu and page pinch-zoom everywhere except inputs, textareas, contenteditable and data-twinkle-selectable text, so text a game shows for copying (a chat log, a word to copy) needs data-twinkle-selectable. A page is a played game when the app sets data-twinkle-game on <html> or <body>, or, for apps that do not lay themselves out with Twinkle.preview, when a play-sized canvas is on screen: rendered, not pointer-events:none, and covering at least 30% of the viewport by its layout size; the page then stays a played game while that canvas is in the document, even hidden between screens. Set data-twinkle-game on a game that sizes itself with Twinkle.preview, a DOM-only game, or a small-canvas game; set data-twinkle-document to keep a canvas page (charts, drawing pad) an ordinary page; set data-twinkle-allow-zoom to keep page pinch-zoom on a game.
|
|
28
|
+
- Played game pages block text selection, the long-press menu and page pinch-zoom everywhere except inputs, textareas, contenteditable and data-twinkle-selectable text, so text a game shows for copying (a chat log, a word to copy) needs data-twinkle-selectable. A page is a played game when the app sets data-twinkle-game on <html> or <body>, or, for apps that do not lay themselves out with Twinkle.preview, when a play-sized canvas is on screen: rendered, not pointer-events:none, and covering at least 30% of the viewport by its layout size; the page then stays a played game while that canvas is in the document, even hidden between screens. Set data-twinkle-game on a game that sizes itself with Twinkle.preview, a DOM-only game, or a small-canvas game; set data-twinkle-document to keep a canvas page (charts, drawing pad) an ordinary page, which also keeps it out of viewport-app layout so it scrolls; set data-twinkle-allow-zoom to keep page pinch-zoom on a game. Viewport-app layout (html/body pinned to the screen, no page scroll) turns on for any canvas except a small one on screen (both sides at most 160 CSS px); data-twinkle-game keeps it for any canvas, and data-twinkle-document or laying the page out with Twinkle.preview turns it off.
|
|
29
29
|
- Keep inputs/contenteditable usable. Mark genuinely copyable story/chat/user-written text data-twinkle-selectable and style it with user-select: text, -webkit-user-select: text, and -webkit-touch-callout: default. SDK defaults have low specificity so existing explicit copyable-text styles remain effective. Preserve document/reader selection; do not block document-wide touch/pointer events or disable scrolling/zoom to prevent selection.
|
|
30
30
|
- Verify mobile long presses on controls, HUD, and empty play space in Safari and Chromium; confirm held controls still work and release/cancel correctly, and scrolling, typing, and copying still work. lumine check only detects a missing no-selection rule, not selector coverage or mobile behavior.
|
|
31
31
|
- Build app tab mute is enforced by the host runtime automatically for standard media elements and Web Audio connections to AudioContext.destination. Apps with custom audio engines can also observe Twinkle.onAudioMuteChange and check Twinkle.isAudioMuted.
|