Merge pull request #5590 from nesquena/release/stage-5558
release #5558: keep Transparent Stream final replies visible after settle
This commit is contained in:
3 files changed
+274
-70
No files matched your search
@@ -14,6 +14,7 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Transparent Stream: the final reply stays a visible answer after the turn settles.** In Transparent Stream mode, the moment a turn settled its final reply folded into the collapsed worklog/activity disclosure group — the answer vanished from view until you refreshed the page (the stored transcript was always correct, so a reload fixed it). The live-anchor scene renderer was hardcoding the compact-worklog layout mode when it projected the settling turn, regardless of your active display mode, so the final answer got compact-worklog display hints and was grouped away. It now reads your actual active mode and applies per-mode display hints, so in Transparent Stream the answer settles as a visible chronological reply. Compact Worklog is unchanged (bit-identical projection). Thanks @rodboev. (#5558, #5550)
|
||||
- **iOS/Android PWA: streaming stays smooth on long chats, and a dropped connection recovers instead of erroring.** Two mobile reliability fixes. (1) On touch devices, the chat now skips layout and paint for off-screen message rows while a response streams (`content-visibility` scoped to `pointer:coarse`), so a long conversation no longer degrades into a freeze on iOS WKWebView — the live streaming turn (targeted by its stable `#liveAssistantTurn` id so every render mode is covered) stays fully rendered so it never blanks mid-stream and the new-message cue still works, off-screen rows keep a tiny size estimate to preserve flick-scroll momentum, and desktop find-in-page is untouched. (2) The SSE reconnect ladder is extended (4→6 backoff steps) with a last-ditch full-session poll (an 8s-watchdog-guarded `_restoreSettledSession`, with a "Restoring session…" affordance) after the retries are exhausted, so a response that finished while the connection was flapping (e.g. an iOS Tailscale/VPN reconnect) is recovered without showing an error banner. Deliberately does not touch `overflow-anchor` — that would re-open the #4856/#5338 mobile jump-to-top regression. Thanks @luperrypf. (#5541)
|
||||
- **Starting a new chat is instant again — memory extraction no longer blocks it, and a managed restart no longer drops that memory.** Creating a new conversation could stall 1–5+ seconds because the previous session's memory-commit (the extraction call to the memory provider) ran synchronously on the request. That commit now runs in the background so "+ New Chat" responds immediately. To keep the deferred work safe across a restart, a `SIGTERM` handler drives an orderly stop so the server drains in-flight memory commits before exiting (previously a managed stop terminated instantly and lost them), the background-commit registry is bounded and self-cleaning, and the drain is wall-clock–bounded so a hung memory provider can't stall the stop (each commit runs under a shared time budget). Thanks @luperrypf. (#5543)
|
||||
- **Auto-scroll now resumes when you return to the bottom of a streaming reply.** If you scrolled up even once while a response was streaming, the chat permanently stopped following new output — scrolling back to the bottom didn't help; only starting a new turn re-engaged auto-follow. Returning to the very bottom of the transcript (within ~80px) now re-pins auto-scroll, so the view follows the stream again. It only re-pins at the true bottom and never while you're actively scrolling (wheel, keyboard, touch, or scrollbar drag), so reading back through history mid-stream still never yanks you down — preserving the #4295 behavior. Thanks @luperrypf. (#5544)
|
||||
|
||||
+33
-4
@@ -2705,12 +2705,35 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
const seq=Number(_assistantSegmentSeq||_currentLiveSegmentSeq||0);
|
||||
return Number.isFinite(seq)&&seq>0?seq:1;
|
||||
}
|
||||
function _anchorSceneActiveMode(){
|
||||
const normalize=value=>value==='transparent_stream'||value==='compact_worklog'?value:'';
|
||||
if(typeof window!=='undefined'){
|
||||
if(typeof window.chatActivityMode==='function'){
|
||||
try{
|
||||
const mode=normalize(window.chatActivityMode());
|
||||
if(mode) return mode;
|
||||
}catch(_){}
|
||||
}
|
||||
const displayMode=normalize(window._chatActivityDisplayMode);
|
||||
if(displayMode) return displayMode;
|
||||
if(window._transparentStream) return 'transparent_stream';
|
||||
}
|
||||
return 'compact_worklog';
|
||||
}
|
||||
function _anchorSceneRowDisplayHintForMode(row, sceneMode){
|
||||
const hints=row&&typeof row==='object'&&row.display_hints&&typeof row.display_hints==='object'
|
||||
? row.display_hints
|
||||
: null;
|
||||
if(sceneMode==='transparent_stream') return (hints&&hints.transparent_stream)||'chronological_activity';
|
||||
if(sceneMode==='compact_worklog') return (hints&&hints.compact_worklog)||row.display_hint||'activity_row';
|
||||
return row&&row.display_hint||'activity_row';
|
||||
}
|
||||
function _renderAnchorLiveScene(){
|
||||
if(!_anchorRegistry||!_isActiveSession()) return false;
|
||||
if(typeof window==='undefined'||typeof window._renderLiveAnchorActivitySceneForStream!=='function') return false;
|
||||
try{
|
||||
return !!window._renderLiveAnchorActivitySceneForStream(streamId, activeSid, {
|
||||
mode:'compact_worklog',
|
||||
mode:_anchorSceneActiveMode(),
|
||||
});
|
||||
}catch(err){
|
||||
if(!_anchorShadowWarned&&typeof console!=='undefined'&&console.warn){
|
||||
@@ -2723,7 +2746,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
function _projectLiveAnchorActivityScene(){
|
||||
if(!_anchorRegistry||!_anchorApi||typeof _anchorApi.projectAssistantTurnAnchorActivityScene!=='function') return null;
|
||||
try{
|
||||
return _anchorApi.projectAssistantTurnAnchorActivityScene(_anchorRegistry,{mode:'compact_worklog'});
|
||||
return _anchorApi.projectAssistantTurnAnchorActivityScene(_anchorRegistry,{mode:_anchorSceneActiveMode()});
|
||||
}catch(_){
|
||||
return null;
|
||||
}
|
||||
@@ -3457,6 +3480,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
}
|
||||
}
|
||||
const base=(projectedScene&&typeof projectedScene==='object')?projectedScene:{};
|
||||
const sceneMode=base.mode==='transparent_stream'?'transparent_stream':_anchorSceneActiveMode();
|
||||
const messageFinalAnswer=_anchorSceneFinalAnswerText(lastAsst);
|
||||
const finalAnswer=_anchorSceneCleanText(messageFinalAnswer)
|
||||
? messageFinalAnswer
|
||||
@@ -3479,7 +3503,12 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
if(key&&seen.has(key)) return;
|
||||
if(key) seen.add(key);
|
||||
if(isTextual&&textKey) seenTextKeys.push(textKey);
|
||||
rows.push({...row,order_index:rows.length,seq:rows.length});
|
||||
rows.push({
|
||||
...row,
|
||||
display_hint:_anchorSceneRowDisplayHintForMode(row,sceneMode),
|
||||
order_index:rows.length,
|
||||
seq:rows.length,
|
||||
});
|
||||
};
|
||||
const projectedRows=Array.isArray(base.activity_rows)?base.activity_rows:[];
|
||||
for(const row of projectedRows){
|
||||
@@ -3496,7 +3525,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
const scene={
|
||||
...base,
|
||||
version:'activity_scene_v1',
|
||||
mode:'compact_worklog',
|
||||
mode:sceneMode,
|
||||
identity:{
|
||||
...((base.identity&&typeof base.identity==='object')?base.identity:{}),
|
||||
source_message_refs:messages.slice(turnStart+1,lastAsstIndex+1)
|
||||
|
||||
@@ -72,24 +72,143 @@ def _run_node_script(script):
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
_EXTRACT_FUNC_JS = """
|
||||
function extractFunc(name){
|
||||
const start = src.indexOf('function ' + name);
|
||||
if(start === -1) throw new Error(name + ' not found');
|
||||
const params = src.indexOf('(', start);
|
||||
let depth = 0, close = -1;
|
||||
for(let i=params; i<src.length; i++){
|
||||
if(src[i] === '(') depth++;
|
||||
else if(src[i] === ')'){
|
||||
depth--;
|
||||
if(depth === 0){ close = i; break; }
|
||||
}
|
||||
}
|
||||
const brace = src.indexOf('{', close);
|
||||
depth = 0;
|
||||
for(let i=brace; i<src.length; i++){
|
||||
if(src[i] === '{') depth++;
|
||||
else if(src[i] === '}'){
|
||||
depth--;
|
||||
if(depth === 0) return src.slice(start, i + 1);
|
||||
}
|
||||
}
|
||||
throw new Error(name + ' body did not close');
|
||||
}
|
||||
""".strip()
|
||||
|
||||
|
||||
def _run_complete_anchor_settlement_case(active_mode):
|
||||
script = f"""
|
||||
const fs = require('fs');
|
||||
const src = fs.readFileSync({json.dumps(str(ROOT / "static" / "messages.js"))}, 'utf8');
|
||||
{_EXTRACT_FUNC_JS}
|
||||
const activeMode = {json.dumps(active_mode)};
|
||||
global.window = {{
|
||||
chatActivityMode(){{ return activeMode; }},
|
||||
_chatActivityDisplayMode: activeMode === 'transparent_stream' ? 'compact_worklog' : 'transparent_stream',
|
||||
_transparentStream: activeMode === 'transparent_stream' ? false : true,
|
||||
}};
|
||||
global.S = {{ session: {{}} }};
|
||||
eval(extractFunc('_anchorSceneCleanText'));
|
||||
eval(extractFunc('_anchorSceneTextKey'));
|
||||
eval(extractFunc('_anchorSceneExistingRowKey'));
|
||||
eval(extractFunc('_anchorSceneRowHasLiveIdentity'));
|
||||
eval(extractFunc('_anchorSceneSettleLiveRunningRow'));
|
||||
eval(extractFunc('_anchorSceneRowLooksLikeFinalAnswer'));
|
||||
eval(extractFunc('_anchorSceneRowTextOverlapsExisting'));
|
||||
eval(extractFunc('_anchorSceneMessageRowsHaveThinking'));
|
||||
if(src.indexOf('function _anchorSceneActiveMode') !== -1){{
|
||||
eval(extractFunc('_anchorSceneActiveMode'));
|
||||
}}else{{
|
||||
eval("function _anchorSceneActiveMode(){{ return activeMode; }}");
|
||||
}}
|
||||
if(src.indexOf('function _anchorSceneRowDisplayHintForMode') !== -1){{
|
||||
eval(extractFunc('_anchorSceneRowDisplayHintForMode'));
|
||||
}}else{{
|
||||
eval("function _anchorSceneRowDisplayHintForMode(row, sceneMode){{ const hints=row&&typeof row==='object'&&row.display_hints&&typeof row.display_hints==='object'?row.display_hints:null; if(sceneMode==='transparent_stream') return (hints&&hints.transparent_stream)||'chronological_activity'; if(sceneMode==='compact_worklog') return (hints&&hints.compact_worklog)||row.display_hint||'activity_row'; return row&&row.display_hint||'activity_row'; }}");
|
||||
}}
|
||||
function _anchorSceneFinalAnswerText(message){{ return message && (message.final_answer || message.content || ''); }}
|
||||
function _anchorSceneRowsByMessageIndex(){{ return new Map(); }}
|
||||
function _anchorSceneMessageRef(message){{ return String(message && message.id || ''); }}
|
||||
function _anchorSceneTurnDurationForSettlement(_lastAsst, base){{ return base && base.turn_duration ? base.turn_duration : 0; }}
|
||||
eval(extractFunc('_completeSettledAnchorSceneForTurn'));
|
||||
const messages = [
|
||||
{{role:'user', content:'Prompt', id:'user-1'}},
|
||||
{{role:'assistant', content:'Final answer text', id:'assistant-1'}},
|
||||
];
|
||||
const projectedScene = {{
|
||||
mode:'compact_worklog',
|
||||
final_answer:'Final answer text',
|
||||
identity:{{source_message_refs:['legacy']}},
|
||||
lifecycle:{{terminal_state:'done'}},
|
||||
turn_duration:42,
|
||||
activity_rows:[
|
||||
{{
|
||||
role:'prose',
|
||||
text:'Final answer text',
|
||||
status:'running',
|
||||
row_id:'live-prose-final',
|
||||
local_id:'live-prose-final',
|
||||
display_hint:'main_prose',
|
||||
display_hints:{{compact_worklog:'main_prose', transparent_stream:'chronological_activity'}},
|
||||
}},
|
||||
{{
|
||||
role:'thinking',
|
||||
text:'Working through the result',
|
||||
status:'running',
|
||||
row_id:'live-thinking-1',
|
||||
local_id:'live-thinking-1',
|
||||
display_hint:'collapsed_thinking',
|
||||
display_hints:{{compact_worklog:'collapsed_thinking', transparent_stream:'chronological_activity'}},
|
||||
}},
|
||||
{{
|
||||
role:'tool',
|
||||
text:'Fetched docs',
|
||||
status:'running',
|
||||
row_id:'live-tool-1',
|
||||
local_id:'live-tool-1',
|
||||
tool_call_id:'tool-1',
|
||||
display_hint:'tool_row',
|
||||
display_hints:{{compact_worklog:'tool_row', transparent_stream:'chronological_activity'}},
|
||||
}},
|
||||
{{
|
||||
role:'terminal',
|
||||
text:'done',
|
||||
status:'completed',
|
||||
row_id:'terminal-1',
|
||||
local_id:'terminal-1',
|
||||
display_hint:'terminal_status_row',
|
||||
display_hints:{{compact_worklog:'terminal_status_row', transparent_stream:'chronological_activity'}},
|
||||
}},
|
||||
],
|
||||
}};
|
||||
const scene = _completeSettledAnchorSceneForTurn(messages, 1, projectedScene);
|
||||
process.stdout.write(JSON.stringify({{
|
||||
mode: scene && scene.mode,
|
||||
final_answer: scene && scene.final_answer,
|
||||
final_message_ref: scene && scene.final_message_ref,
|
||||
identity: scene && scene.identity,
|
||||
activity_rows: scene && scene.activity_rows ? scene.activity_rows.map(row => ({{
|
||||
role: row.role,
|
||||
text: row.text,
|
||||
status: row.status,
|
||||
display_hint: row.display_hint,
|
||||
compact_hint: row.display_hints && row.display_hints.compact_worklog,
|
||||
transparent_hint: row.display_hints && row.display_hints.transparent_stream,
|
||||
}})) : null,
|
||||
}}));
|
||||
"""
|
||||
return _run_node_script(script)
|
||||
|
||||
|
||||
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
||||
def test_dom_render_live_compression_row_transitions_to_settled_scene():
|
||||
script = f"""
|
||||
const fs = require('fs');
|
||||
const src = fs.readFileSync({json.dumps(str(ROOT / "static" / "ui.js"))}, 'utf8');
|
||||
function extractFunc(name){{
|
||||
const re = new RegExp('function\\\\s+' + name + '\\\\s*\\\\(');
|
||||
const start = src.search(re);
|
||||
if(start < 0) throw new Error(name + ' not found');
|
||||
let i = src.indexOf('{{', start) + 1;
|
||||
let depth = 1;
|
||||
while(depth > 0 && i < src.length){{
|
||||
if(src[i] === '{{') depth += 1;
|
||||
else if(src[i] === '}}') depth -= 1;
|
||||
i += 1;
|
||||
}}
|
||||
return src.slice(start, i);
|
||||
}}
|
||||
{_EXTRACT_FUNC_JS}
|
||||
class FakeElement {{
|
||||
constructor(tag){{
|
||||
this.tagName = String(tag || 'div').toUpperCase();
|
||||
@@ -231,15 +350,68 @@ def test_live_ui_legacy_paths_exit_when_anchor_scene_owns_the_turn():
|
||||
assert ':not([data-anchor-scene-owner="1"])' in remove
|
||||
|
||||
|
||||
def test_anchor_scene_projection_stays_scoped_to_compact_worklog():
|
||||
@pytest.mark.skipif("_anchorSceneActiveMode()" not in MESSAGES_JS, reason="base branch lacks the active-mode helper")
|
||||
def test_anchor_scene_projection_tracks_active_mode():
|
||||
render_live = _function_body(MESSAGES_JS, "_renderAnchorLiveScene")
|
||||
project_live = _function_body(MESSAGES_JS, "_projectLiveAnchorActivityScene")
|
||||
ui_live = _function_body(UI_JS, "_renderLiveAnchorActivitySceneForStream")
|
||||
|
||||
assert "mode:'compact_worklog'" in render_live
|
||||
assert "mode:'compact_worklog'" in project_live
|
||||
assert "(opts&&opts.mode)||'compact_worklog'" in ui_live
|
||||
assert "if(typeof isCompactWorklogMode==='function'&&!isCompactWorklogMode()) return false;" in _function_body(UI_JS, "renderLiveAnchorActivityScene")
|
||||
assert "_anchorSceneActiveMode()" in render_live
|
||||
assert "_anchorSceneActiveMode()" in project_live
|
||||
assert "mode:'compact_worklog'" not in render_live
|
||||
assert "mode:'compact_worklog'" not in project_live
|
||||
|
||||
|
||||
@pytest.mark.skipif("_anchorSceneActiveMode()" not in MESSAGES_JS, reason="base branch lacks the active-mode helper")
|
||||
def test_anchor_scene_active_mode_falls_back_when_primary_accessor_throws():
|
||||
script = f"""
|
||||
const fs = require('fs');
|
||||
const src = fs.readFileSync({json.dumps(str(ROOT / "static" / "messages.js"))}, 'utf8');
|
||||
{_EXTRACT_FUNC_JS}
|
||||
eval(extractFunc('_anchorSceneActiveMode'));
|
||||
global.window = {{
|
||||
chatActivityMode(){{ throw new Error('mode unavailable'); }},
|
||||
_chatActivityDisplayMode:'transparent_stream',
|
||||
_transparentStream:false,
|
||||
}};
|
||||
const displayModeFallback = _anchorSceneActiveMode();
|
||||
global.window = {{
|
||||
chatActivityMode(){{ throw new Error('mode unavailable'); }},
|
||||
_chatActivityDisplayMode:'broken',
|
||||
_transparentStream:true,
|
||||
}};
|
||||
const booleanFallback = _anchorSceneActiveMode();
|
||||
process.stdout.write(JSON.stringify({{displayModeFallback, booleanFallback}}));
|
||||
"""
|
||||
result = _run_node_script(script)
|
||||
|
||||
assert result["displayModeFallback"] == "transparent_stream"
|
||||
assert result["booleanFallback"] == "transparent_stream"
|
||||
|
||||
|
||||
def test_complete_settled_anchor_scene_tracks_transparent_stream_and_filters_duplicate_final_answer():
|
||||
result = _run_complete_anchor_settlement_case("transparent_stream")
|
||||
|
||||
assert result["mode"] == "transparent_stream"
|
||||
assert result["final_answer"] == "Final answer text"
|
||||
assert result["final_message_ref"] == "assistant-1"
|
||||
assert result["identity"]["source_message_refs"] == ["assistant-1"]
|
||||
assert [row["role"] for row in result["activity_rows"]] == ["thinking", "tool", "terminal"]
|
||||
assert [row["status"] for row in result["activity_rows"]] == ["completed", "completed", "completed"]
|
||||
assert [row["display_hint"] for row in result["activity_rows"]] == [
|
||||
"chronological_activity",
|
||||
"chronological_activity",
|
||||
"chronological_activity",
|
||||
]
|
||||
assert [row["compact_hint"] for row in result["activity_rows"]] == [
|
||||
"collapsed_thinking",
|
||||
"tool_row",
|
||||
"terminal_status_row",
|
||||
]
|
||||
assert [row["transparent_hint"] for row in result["activity_rows"]] == [
|
||||
"chronological_activity",
|
||||
"chronological_activity",
|
||||
"chronological_activity",
|
||||
]
|
||||
|
||||
|
||||
def test_live_processed_anchor_renders_before_first_activity_row():
|
||||
@@ -333,6 +505,53 @@ def test_live_processed_anchor_is_deduped_across_restore_paths():
|
||||
assert "_dedupeLiveProcessedWorklogAnchors($('liveAssistantTurn'))" in shell
|
||||
|
||||
|
||||
def test_complete_settled_anchor_scene_keeps_compact_worklog_rows_in_compact_mode():
|
||||
result = _run_complete_anchor_settlement_case("compact_worklog")
|
||||
|
||||
assert result["mode"] == "compact_worklog"
|
||||
assert result["final_answer"] == "Final answer text"
|
||||
assert [row["role"] for row in result["activity_rows"]] == ["thinking", "tool", "terminal"]
|
||||
assert [row["display_hint"] for row in result["activity_rows"]] == [
|
||||
"collapsed_thinking",
|
||||
"tool_row",
|
||||
"terminal_status_row",
|
||||
]
|
||||
assert [row["compact_hint"] for row in result["activity_rows"]] == [
|
||||
"collapsed_thinking",
|
||||
"tool_row",
|
||||
"terminal_status_row",
|
||||
]
|
||||
assert [row["transparent_hint"] for row in result["activity_rows"]] == [
|
||||
"chronological_activity",
|
||||
"chronological_activity",
|
||||
"chronological_activity",
|
||||
]
|
||||
|
||||
|
||||
def test_anchor_scene_has_worklog_worthy_rows_rejects_prose_only_and_terminal_only_scenes():
|
||||
body = _function_body(MESSAGES_JS, "_anchorSceneHasWorklogWorthyRows")
|
||||
|
||||
assert "role==='tool'||role==='thinking'" in body
|
||||
assert "source==='compressing'||source==='compressed'" in body
|
||||
|
||||
script = f"""
|
||||
const fs = require('fs');
|
||||
const src = fs.readFileSync({json.dumps(str(ROOT / "static" / "messages.js"))}, 'utf8');
|
||||
{_EXTRACT_FUNC_JS}
|
||||
eval(extractFunc('_anchorSceneHasWorklogWorthyRows'));
|
||||
const proseOnly = {{activity_rows:[{{role:'prose'}}, {{role:'terminal', source_event_type:'done'}}]}};
|
||||
const terminalOnly = {{activity_rows:[{{role:'terminal', source_event_type:'done'}}]}};
|
||||
process.stdout.write(JSON.stringify({{
|
||||
proseOnly: _anchorSceneHasWorklogWorthyRows(proseOnly),
|
||||
terminalOnly: _anchorSceneHasWorklogWorthyRows(terminalOnly),
|
||||
}}));
|
||||
"""
|
||||
result = _run_node_script(script)
|
||||
|
||||
assert result["proseOnly"] is False
|
||||
assert result["terminalOnly"] is False
|
||||
|
||||
|
||||
def test_scene_renderer_coalesces_row_updates_and_renders_in_scene_order():
|
||||
rows = _function_body(UI_JS, "_anchorSceneRowsForRendering")
|
||||
render = _function_body(UI_JS, "_renderAnchorSceneRowsIntoWorklog")
|
||||
@@ -355,29 +574,7 @@ def test_live_anchor_scene_dedupes_exact_duplicate_process_prose_only_live():
|
||||
script = f"""
|
||||
const fs = require('fs');
|
||||
const src = fs.readFileSync({json.dumps(str(ROOT / "static" / "ui.js"))}, 'utf8');
|
||||
function extractFunc(name){{
|
||||
const start = src.indexOf('function ' + name);
|
||||
if(start === -1) throw new Error(name + ' not found');
|
||||
const params = src.indexOf('(', start);
|
||||
let depth = 0, close = -1;
|
||||
for(let i=params; i<src.length; i++){{
|
||||
if(src[i] === '(') depth++;
|
||||
else if(src[i] === ')'){{
|
||||
depth--;
|
||||
if(depth === 0){{ close = i; break; }}
|
||||
}}
|
||||
}}
|
||||
const brace = src.indexOf('{{', close);
|
||||
depth = 0;
|
||||
for(let i=brace; i<src.length; i++){{
|
||||
if(src[i] === '{{') depth++;
|
||||
else if(src[i] === '}}'){{
|
||||
depth--;
|
||||
if(depth === 0) return src.slice(start, i + 1);
|
||||
}}
|
||||
}}
|
||||
throw new Error(name + ' body did not close');
|
||||
}}
|
||||
{_EXTRACT_FUNC_JS}
|
||||
function _anchorSceneToolRowLogicalKey(){{ return ''; }}
|
||||
function _anchorSceneMergeToolRows(a,b){{ return b; }}
|
||||
function _anchorSceneIsSettledSuccessfulCompression(){{ return false; }}
|
||||
@@ -846,30 +1043,7 @@ def test_live_anchor_scene_transparent_snapshot_render_is_idempotent_and_hides_l
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const src = fs.readFileSync({json.dumps(str(ROOT / "static" / "ui.js"))}, 'utf8');
|
||||
function extractFunc(name){{
|
||||
const re = new RegExp('function\\\\s+' + name + '\\\\s*\\\\(');
|
||||
const start = src.search(re);
|
||||
if(start < 0) throw new Error(name + ' not found');
|
||||
const params = src.indexOf('(', start);
|
||||
let depth = 0, close = -1;
|
||||
for(let i=params; i<src.length; i++){{
|
||||
if(src[i] === '(') depth++;
|
||||
else if(src[i] === ')'){{
|
||||
depth--;
|
||||
if(depth === 0){{ close = i; break; }}
|
||||
}}
|
||||
}}
|
||||
const brace = src.indexOf('{{', close);
|
||||
depth = 0;
|
||||
for(let i=brace; i<src.length; i++){{
|
||||
if(src[i] === '{{') depth++;
|
||||
else if(src[i] === '}}'){{
|
||||
depth--;
|
||||
if(depth === 0) return src.slice(start, i + 1);
|
||||
}}
|
||||
}}
|
||||
throw new Error(name + ' body did not close');
|
||||
}}
|
||||
{_EXTRACT_FUNC_JS}
|
||||
|
||||
class FakeElement {{
|
||||
constructor(tag='div'){{
|
||||
|
||||
Reference in new issue
Block a user