#!/usr/bin/env python3 """Each box can be clicked shut, and the live line is revealed, not dumped. python3 tests/test_vm_live_view_collapse.py WHAT WAS ASKED FOR * the live line above the task list should arrive a few characters at a time, so that its speed is the token rate rather than a poll artefact; * clicking the task list should leave two lines - that live line, and the step the session is on; * clicking the screen should halve it, on black; * clicking the feed should halve its height; * clicking a second time should put any of them back. THREE THINGS HERE WERE MEASURED IN A BROWSER, NOT REASONED ABOUT 1. `window.name` SURVIVES THE srcdoc SWAP AND NOTHING ELSE DOES. The view is re-sent every couple of seconds and each send replaces the frame's srcdoc, destroying the document. The frame is sandboxed without `allow-same-origin`, so it has an opaque origin: localStorage and cookies throw and there is no listener in Open WebUI to postMessage to. `window.name` belongs to the browsing context, which survives. Measured in that exact sandbox: the third load read back the name its two predecessors wrote. Without it a box springs open a second after being clicked shut. 2. THE STATE IS KEYED BY WHICH BOX IT IS. The screen box appears mid-session and pushes the feed down a slot, so the frame that was the feed becomes the screen - and would inherit its state. 3. `flex:none` ON THE FEED ROWS IS LOAD-BEARING. The rows sit in a flex column so a collapsed feed keeps its newest lines against the bottom. Flex items shrink by default, so without it the rows compressed into the smaller box and the text overlapped itself: measured, twenty lines crushed into ten lines of space instead of ten lines shown and ten lost off the top. Browser-verified, in a real sandboxed frame, before this file was written: task box 196px -> 51px and back; screen 500px -> 269px with the image exactly half its width on black; feed 364px -> 190px showing ten rows of an unchanged 17.4px. All three survived two srcdoc swaps. """ import asyncio import json import os import re import sys import types HERE = os.path.dirname(os.path.abspath(__file__)) FILTER = os.path.join(HERE, "..", "owui-functions", "vm_agent_progress.py") FAILS: list[str] = [] def check(label: str, condition: bool, detail: str = "") -> None: if condition: print(f"ok {label}") else: FAILS.append(f"{label}: {detail}") print(f"FAIL {label}\n {detail}") src = open(FILTER, encoding="utf-8").read() aiohttp = types.ModuleType("aiohttp") aiohttp.ClientError = Exception aiohttp.ClientSession = object aiohttp.ClientTimeout = object sys.modules["aiohttp"] = aiohttp pydantic = types.ModuleType("pydantic") class _BaseModel: def __init__(self, **kw): for key, value in kw.items(): setattr(self, key, value) pydantic.BaseModel = _BaseModel pydantic.Field = lambda default=None, **kw: default sys.modules["pydantic"] = pydantic namespace: dict = {} exec(compile(src, FILTER, "exec"), namespace) Filter = namespace["Filter"] class Valves: show_terminal = True show_screenshots = True DATA = { "tasks": "\n".join(f"[ ] {i + 1}. step {i + 1}" for i in range(6)), "status": "3/6 ยท the step it is on", "speech": "the model's own voice, mid-sentence", "shot": "data:image/jpeg;base64,AAAA", "caption": "the desktop", "terminal": ["$ one", "$ two"], } out: list = [] async def _run(): async def emitter(event): out.append(event) await Filter._embed(emitter, DATA, Valves()) asyncio.run(_run()) boxes = out[-1]["data"]["embeds"] check("three boxes once there is a screenshot", len(boxes) == 3, str(len(boxes))) TASKS, SCREEN, FEED = boxes def config(box: str) -> dict: found = re.search(r'', box, re.S) return json.loads(found.group(1).replace("<\\/", "")) if found else {} # --- every box says which one it is --------------------------------------- check("each box carries its own config", all(config(b) for b in boxes)) check("and they are the three kinds, in order", [config(b).get("k") for b in boxes] == ["tasks", "screen", "feed"], str([config(b).get("k") for b in boxes])) check("the config is JSON, not generated JavaScript", 'type="application/json"' in TASKS, "the live line is the model's own words - a quote in it would end the script") raw_cfg = re.search(r'id="cfg">(.*?)', TASKS, re.S).group(1) check("and a closing tag inside it cannot end the script early", "" not in raw_cfg, "the HTML parser ends the block at the first , wherever it appears, " "so the JSON escapes it as <\\/ - which JSON reads back as a plain slash") # --- the task box collapses to two lines ---------------------------------- check("the task box has a live line, the list, and the current step", all(x in TASKS for x in ('id="live"', 'id="full"', 'id="cur"'))) check("the current step is the one the collapsed box will show", "the step it is on" in TASKS, "collapsed, the second line is the status - so it has to be in the box") check("the step line is hidden until the box is clicked", 'id="cur" style="display:none' in TASKS) check("the live line is right-anchored, because the server sends the tail", "justify-content:flex-end" in TASKS, "the end of the sentence is the part that is moving") check("and it holds its line even before the model has spoken", "min-height" in TASKS, "otherwise the collapsed box is one line until the first token") # --- the screen box halves onto black ------------------------------------- check("the screen box has a wrapper to blacken and an image to shrink", 'id="wrap"' in SCREEN and 'id="shot"' in SCREEN) # --- the feed box halves its height --------------------------------------- rows = FEED.count('