mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] canvas: an app window owns its wheel whether or not you clicked in first; workflow tests wait on progress, not a clock
This commit is contained in:
@@ -546,10 +546,13 @@ def handle_delete_step(args: dict) -> dict:
|
||||
|
||||
# How long a synchronous test may hold the turn. Long enough for a real multi-step workflow, short
|
||||
# enough that a wedged test returns an honest "still running" instead of hanging the conversation.
|
||||
# A real workflow test does several searches and page fetches per step. The first budget was 240s
|
||||
# and a live three-step digest took 243, so it missed by three seconds and the agent had to make a
|
||||
# second hop anyway, which is the exact re-pinging this tool exists to remove.
|
||||
TEST_WAIT_S = 600
|
||||
# Wait on PROGRESS, not on a clock. A fixed budget answers the wrong question: a run doing real work
|
||||
# should never be cut off (the first budget was 240s and a live digest took 243, missing by three
|
||||
# seconds), while a run that is genuinely wedged should not be waited on for ten minutes either. So
|
||||
# the deadline resets every time the transcript grows, and only silence ends the wait.
|
||||
TEST_IDLE_S = 180.0
|
||||
# Absolute backstop for the pathological case where a run reports progress forever.
|
||||
TEST_MAX_S = 3600.0
|
||||
TEST_POLL_S = 3
|
||||
|
||||
|
||||
@@ -565,33 +568,39 @@ def handle_test_workflow(args: dict) -> dict:
|
||||
# to "call ReadTestTranscript once it finishes", but a model has no way to know when that is, so
|
||||
# it ended its turn and the HUMAN had to keep re-pinging it. A test whose result the caller
|
||||
# cannot observe is not a tool, it is homework for the user.
|
||||
deadline = time.time() + TEST_WAIT_S
|
||||
started = time.time()
|
||||
last_progress_at = started
|
||||
last_len = -1
|
||||
last_status = "running"
|
||||
while time.time() < deadline:
|
||||
partial = ""
|
||||
while True:
|
||||
now = time.time()
|
||||
if now - last_progress_at >= TEST_IDLE_S or now - started >= TEST_MAX_S:
|
||||
break
|
||||
time.sleep(TEST_POLL_S)
|
||||
t = _call("GET", f"/{wid}/test-transcript")
|
||||
if "_error" in t:
|
||||
continue
|
||||
last_status = t.get("status") or "running"
|
||||
transcript = t.get("transcript") or ""
|
||||
# Any growth in the transcript is the run telling us it is alive, so the clock starts over.
|
||||
if len(transcript) != last_len:
|
||||
last_len = len(transcript)
|
||||
last_progress_at = time.time()
|
||||
partial = transcript
|
||||
if last_status in ("running", "none"):
|
||||
continue
|
||||
transcript = t.get("transcript") or "(empty transcript)"
|
||||
return _ok(f"Test finished (status: {last_status}). Transcript:\n\n{transcript}")
|
||||
return _ok(f"Test finished (status: {last_status}). Transcript:\n\n{transcript or '(empty transcript)'}")
|
||||
# Hand back whatever the run has actually produced. Returning only "call ReadTestTranscript" made
|
||||
# the model guess when to poll, which is the same dead end as not waiting at all; a partial
|
||||
# transcript is something it can reason about right now.
|
||||
partial = ""
|
||||
try:
|
||||
t = _call("GET", f"/{wid}/test-transcript")
|
||||
if "_error" not in t:
|
||||
partial = (t.get("transcript") or "").strip()
|
||||
except Exception:
|
||||
partial = ""
|
||||
waited = int(time.time() - started)
|
||||
head = (
|
||||
f"Test Agent (session {sid[:8]}) has not finished after {TEST_WAIT_S}s (status: {last_status}). "
|
||||
"Everything it has produced so far follows; call ReadTestTranscript for the rest once it lands."
|
||||
f"Test Agent (session {sid[:8]}) went quiet: no new output for {int(TEST_IDLE_S)}s "
|
||||
f"(status: {last_status}, waited {waited}s total). Everything it produced follows; "
|
||||
"call ReadTestTranscript if it lands later."
|
||||
)
|
||||
return _ok(f"{head}\n\n{partial}" if partial else head)
|
||||
return _ok(f"{head}\n\n{partial.strip()}" if partial.strip() else head)
|
||||
|
||||
|
||||
def handle_read_test_transcript(args: dict) -> dict:
|
||||
|
||||
@@ -9,6 +9,11 @@ import { applyBrowserZoom } from '@/shared/browserZoom';
|
||||
import { syncTiledGeometry } from '../../canvas/tiledGeometry';
|
||||
import { revealZoom, REVEAL_MIN_ZOOM } from '../../canvas/revealZoom';
|
||||
|
||||
// Surfaces that are WINDOWS, not canvas cards: they behave like an OS window, so a wheel inside one
|
||||
// belongs to it whether or not you clicked in first. Canvas cards (agent, browser, view) keep the
|
||||
// Google Maps model instead, where a plain scroll over an unfocused card drives the canvas.
|
||||
const APP_WINDOW_SELECT_TYPES = new Set(['settings-card', 'marketplace-card', 'workflows-hub-card']);
|
||||
|
||||
const MIN_ZOOM = 0.15;
|
||||
// The floor for AUTOMATIC reveals only. revealCards takes min(current, fit), which can only ever go
|
||||
// down, so every spawn that did not fit ratcheted the camera out and nothing ever brought it back:
|
||||
@@ -356,12 +361,13 @@ export function useCanvasControls(
|
||||
// old walk-up handed those to the canvas: reaching the end of Settings zoomed the world out.
|
||||
const windowEl = (e.target as HTMLElement | null)?.closest?.('[data-select-type]') as HTMLElement | null;
|
||||
if (windowEl && !(e.ctrlKey || e.metaKey)) {
|
||||
// Only a card you have clicked INTO owns the wheel. Claiming every card unconditionally
|
||||
// killed the Google Maps model: with cards under the pointer the canvas stopped responding
|
||||
// to scroll at all. Singleton windows (Settings, Marketplace) carry no select-id and are
|
||||
// always owners, since their inner panels are exactly the "hit target isn't the scroller" case.
|
||||
// An app WINDOW always owns its wheel; a canvas CARD only owns it once you have clicked in.
|
||||
// That split is the whole rule. Requiring click-focus for windows too meant hovering over
|
||||
// Settings and scrolling leaked straight to the canvas, because nothing had focused it yet,
|
||||
// and windows do carry a select-id so a "no id means a window" test silently never fired.
|
||||
const windowId = windowEl.getAttribute('data-select-id');
|
||||
if (!windowId || windowId === getScrollFocusedCard()) {
|
||||
const isAppWindow = APP_WINDOW_SELECT_TYPES.has(windowEl.getAttribute('data-select-type') || '');
|
||||
if (isAppWindow || !windowId || windowId === getScrollFocusedCard()) {
|
||||
containedEl = windowEl;
|
||||
containedAt = Date.now();
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user