Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
b940dd3
Run task_live's hosted roles on the managed default model
YellowSnnowmann Oct 6, 2026
c4f76f9
Pick the suggestion an autocomplete box lists for entered text
YellowSnnowmann Oct 6, 2026
5be352c
Recognise login walls that name what they hide
YellowSnnowmann Oct 6, 2026
4c626bc
Do not take the reCAPTCHA badge for a captcha to solve
YellowSnnowmann Oct 6, 2026
46e2786
Let task_live wait for the person at the terminal
YellowSnnowmann Oct 6, 2026
ed3242c
Never press a panel that strings its suggestions together
YellowSnnowmann Oct 6, 2026
b08bc1e
Read the rows inside a panel instead of the panel as one button
YellowSnnowmann Oct 6, 2026
fa9cc9e
Ask a request too large for one Jev call in parts
YellowSnnowmann Oct 6, 2026
0d195f5
Ride out a provider's brief outage before failing a decision
YellowSnnowmann Oct 6, 2026
7b6532d
Gate a store's and a ride app's last button
YellowSnnowmann Oct 6, 2026
493b763
Stop waiting when the page says it found nothing
YellowSnnowmann Oct 6, 2026
5e27b56
Report values read into variables the flow declared
YellowSnnowmann Oct 6, 2026
c2a3e86
Print what a task read wherever task_live stops
YellowSnnowmann Oct 6, 2026
ea910cb
Tell the planner a size or colour is a choose, not a pick
YellowSnnowmann Oct 6, 2026
8662569
Tell the planner a condition names a state, not a change
YellowSnnowmann Oct 6, 2026
8f11f5c
End a pause's reason with one full stop in task_live's prompts
YellowSnnowmann Oct 6, 2026
8d23331
Keep a region the page makes pressable itself a button
YellowSnnowmann Oct 6, 2026
63e804b
Keep the typed text when no suggestion clearly fits it
YellowSnnowmann Oct 6, 2026
80b8892
Tell the planner a result filter belongs in its pick
YellowSnnowmann Oct 6, 2026
1140143
Tally every part of a split request, not only the first
YellowSnnowmann Oct 6, 2026
d0e79bf
Tell the planner to keep the task's item and criterion in a pick
YellowSnnowmann Oct 6, 2026
8d91451
Take the first ranked item that belongs to the list a pick names
YellowSnnowmann Oct 6, 2026
60e7879
Find result lists drawn as single controls, and skip bare markers
YellowSnnowmann Oct 6, 2026
f1f098d
Pause at OTP sign-in dialogs and "Sign up or Log in" walls
YellowSnnowmann Oct 6, 2026
61a89c2
Read more of what a page draws as controls
YellowSnnowmann Oct 6, 2026
4560be4
Keep a press in the tab and get it past covers it did not mean
YellowSnnowmann Oct 6, 2026
1968290
Teach the planner the store, dialog, seat, and date patterns
YellowSnnowmann Oct 6, 2026
1bda38e
Keep the task's own dialog open and stop presses that repeat
YellowSnnowmann Oct 6, 2026
43967f9
Type what plain steps type, and pick suggestions, lists, and dates cl…
YellowSnnowmann Oct 6, 2026
428fb3b
Document the new step, suggestion, and threshold behaviour
YellowSnnowmann Oct 6, 2026
fec1cca
Leave a pick the page already has selected as it is
YellowSnnowmann Oct 6, 2026
a7a5306
Tell the planner a stepper in place of Add means the item is in the cart
YellowSnnowmann Oct 6, 2026
6e6abc4
Let a focused box go before a covered retry; keep rescues off added i…
YellowSnnowmann Oct 6, 2026
9dc611f
Take a place box's closest row when none names the place exactly
YellowSnnowmann Oct 6, 2026
18b2f82
Read a table's rows and buttons the way a person sees them
YellowSnnowmann Oct 6, 2026
86337b9
Keep the step after a rescue that reruns the failed one
YellowSnnowmann Oct 6, 2026
25331ad
Take the list Jev leaned to, and drop a list split from another's cards
YellowSnnowmann Oct 6, 2026
a390a15
Read the items chosen together in one list as one source
YellowSnnowmann Oct 6, 2026
9418146
Let a step that chooses several items press each one's own copy
YellowSnnowmann Oct 6, 2026
f55b43c
Answer the task's own dialog when nothing serves the step
YellowSnnowmann Oct 6, 2026
b054d7d
Keep a choice's conditions, and press a booking button first
YellowSnnowmann Oct 6, 2026
6f24205
Name a phone check and a robot challenge as walls; test first and las…
YellowSnnowmann Oct 6, 2026
1fcfd17
Keep a press to what was pressed, and read a page more carefully
YellowSnnowmann Oct 6, 2026
35e4663
Fix the flow engine's review findings: secrets, dialogs, presses, picks
YellowSnnowmann Oct 6, 2026
76ea846
Test the drawn-page, covered-press, and link-follow paths
YellowSnnowmann Oct 6, 2026
4b93e75
Never take a typed-into box for a suggestion row
YellowSnnowmann Oct 6, 2026
c3c9a5a
Print page words safely in the runner's terminal prompts
YellowSnnowmann Oct 6, 2026
657fffb
Say which model ids the Tiny Humans gateway refuses
YellowSnnowmann Oct 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,27 @@ TINYCOMPUTER_LAB_SELF_EMAIL=
# task_live: 1 shows the browser the task launches instead of running it
# headless (a headed run needs a display, so it runs on the host).
# TASK_HEADED=1
# task_live: 1 waits for the person at the terminal where only a person can
# go on (approve, log in or solve a captcha, type a missing detail, pay)
# instead of ending the run there.
# TASK_INTERACTIVE=1
# The travel fixture's address for browser_fixture.
# TINYCOMPUTER_FIXTURE_URL=http://127.0.0.1:8000
# task_live: a Tiny Humans bearer (a session token, or an API key with the
# `inference` scope) in place of OPENROUTER_API_KEY; Jev and the planner then
# go through Tiny Humans' routes, and the gateway's agentic-v1 plans, rescues,
# and shapes unless the model variables below name another.
# go through Tiny Humans' routes, and openrouter/deepseek/deepseek-v4-flash
Comment thread
YellowSnnowmann marked this conversation as resolved.
Comment thread
YellowSnnowmann marked this conversation as resolved.
# plans, rescues, and shapes unless the model variables below name another.
# TINYHUMANS_TOKEN=
# The planner model task_live asks for.
# TINYCOMPUTER_PLANNER_MODEL=
# The reasoning model task_live rescues a failed step with (default
# openai/gpt-6-luna), and how many rescues a task gets (0 to 5, default 5).
# openai/gpt-6-luna on OpenRouter), and how many rescues a task gets (0 to 5,
# default 5).
# TINYCOMPUTER_RESCUE_MODEL=
# TASK_RESCUES=5
# The reasoning model task_live shapes a finished task's answer with
# (default openai/gpt-6-luna), and a JSON TaskOutput file asking for it.
# (default openai/gpt-6-luna on OpenRouter), and a JSON TaskOutput file
# asking for it.
# TINYCOMPUTER_OUTPUT_MODEL=
# OUTPUT_FILE=crates/tinycomputer-examples/tasks/whatsapp/output.json
# Which decision model task_live's module asks: TINYCOMPUTER_DECISIONS=sage
Expand Down
25 changes: 25 additions & 0 deletions crates/tinycomputer-browser/src/fake/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,31 @@ impl Fake {
.collect()
}

/// Every command sent, in order.
pub(crate) fn sent(&self) -> Vec<Value> {
self.sent.lock().unwrap().clone()
}

/// Whether a page script ran besides the one that keeps a press in the
/// tab, which runs before every click.
pub(crate) fn evaluated_besides_keeping_the_tab(&self) -> bool {
self.sent().iter().any(|command| {
command["action"] == "evaluate"
&& !command["script"]
.as_str()
.unwrap_or_default()
.contains("__tcOpen")
})
}

/// Whether the pointer pressed anywhere by position, rather than only
/// moving.
pub(crate) fn pressed_by_position(&self) -> bool {
self.sent()
.iter()
.any(|command| command["action"] == "mouse" && command["eventType"] != "mouseMoved")
}

pub(crate) fn last(&self, action: &str) -> Value {
self.sent
.lock()
Expand Down
56 changes: 56 additions & 0 deletions crates/tinycomputer-browser/src/surface/location.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! Letting a page read where the person is, when the agent presses the
//! page's own "use my current location" button.

use serde_json::json;
use tinycomputer_core::surface::Candidate;

use super::BrowserSurface;

/// Words of a control that asks the page to find where the person is.
const ASKS_WHERE: &[&str] = &[
"current location",
"my location",
"detect location",
"detect my location",
"locate me",
"use location",
"use my current",
];

/// Whether pressing `target` asks the page for the person's location.
pub(super) fn asks_where(target: &Candidate) -> bool {
let said = format!(
"{} {}",
target.name.as_deref().unwrap_or_default(),
target.description.as_deref().unwrap_or_default()
)
.to_lowercase()
.replace('-', " ");
ASKS_WHERE.iter().any(|words| said.contains(words))
}

impl BrowserSurface {
/// Grants the session's pages the location permission before a press
/// that asks for it (`asks_where`). The browser asks a person in a
/// bubble outside the page, which the agent can neither see nor press,
/// so a store's "use my current location" waited on it forever; the
/// press stands in for that person's "Allow". Best effort: a browser
/// that refuses is pressed as it is.
///
/// Only in a browser the module launched on a throwaway profile: the
/// grant covers every page of the browser for the session, and a
/// person's own browser (an `endpoint`) or profile (a `user_data_dir`)
/// keeps its own say, in the bubble they can answer.
pub(super) fn allow_location(&self) {
if self.options.endpoint.is_some() || self.options.user_data_dir.is_some() {
return;
}
let Ok(id) = self.ensure_session() else {
return;
};
let _granted = self.block(self.browser.command(
&id,
json!({"action": "permissions", "permissions": ["geolocation"]}),
));
}
}
20 changes: 16 additions & 4 deletions crates/tinycomputer-browser/src/surface/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ mod card;
mod cursor;
mod envelope;
mod fields;
mod location;
mod native_select;
mod operations;
mod sight;
mod tabs;
mod tree;
mod uncover;

pub use sight::Denoised;

Expand Down Expand Up @@ -48,6 +51,12 @@ const SETTLE_MS: u64 = 400;
/// that polls forever is never idle, so this is a cap, not an expectation.
const NETWORK_IDLE_MS: u64 = 2_000;

/// The longest one reading of the page may take, by sight or as a tree. A
/// reading sent while a page was being replaced waited out the browser's
/// own deadline live, 30 s for sight and again for the tree, so one look
/// took a minute; a reading this late is retried on the next look instead.
const READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);

/// How a [`BrowserSurface`] reads a page.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Perception {
Expand Down Expand Up @@ -185,11 +194,14 @@ impl BrowserSurface {
fn see(&self, root: Option<&str>) -> Option<Screen> {
self.keep_denoised(Denoised::default());
let id = self.ensure_session().ok()?;
// The timer is made inside the runtime `block` enters, not before.
let reading = self.browser.command(
&id,
json!({"action": "evaluate", "script": sight::script(root)}),
);
let reply = self
.block(self.browser.command(
&id,
json!({"action": "evaluate", "script": sight::script(root)}),
))
.block(async { tokio::time::timeout(READ_TIMEOUT, reading).await })
.ok()?
.ok()?;
let result = reply.get("result")?;
let screen = sight::screen(result)?;
Expand Down
56 changes: 27 additions & 29 deletions crates/tinycomputer-browser/src/surface/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ use tinycomputer_bus::{DesktopError, DesktopResponse, JevOperation};
use tinycomputer_core::surface::{Candidate, Depth, Screen, Surface, uses_pointer};
use tinycomputer_core::{Key, Platform};

use super::card::selects_on_click;
use super::envelope::{covered, failure, not_a_text_field, reply};
use crate::error::Error;

use super::envelope::{failure, not_a_text_field, reply};
use super::sight;
use super::{BrowserSurface, Perception};
use super::{NETWORK_IDLE_MS, SETTLE_MS, SKELETON_DEPTH, tree};
use super::{NETWORK_IDLE_MS, READ_TIMEOUT, SETTLE_MS, SKELETON_DEPTH, tree};

impl Surface for BrowserSurface {
fn observe(
Expand All @@ -37,7 +38,16 @@ impl Surface for BrowserSurface {
};
let snapshot = self
.ensure_session()
.and_then(|id| self.block(self.browser.snapshot(&id, request)))
.and_then(|id| {
let reading = self.browser.snapshot(&id, request);
self.block(async { tokio::time::timeout(READ_TIMEOUT, reading).await })
.unwrap_or_else(|_| {
Err(Error::timeout(
"snapshot",
u64::try_from(READ_TIMEOUT.as_millis()).unwrap_or(u64::MAX),
))
})
})
.map_err(|error| Box::new(failure("snapshot", &error)))?;
let mut screen = tree::screen(&snapshot.tree, &snapshot.title);
if !app.is_empty() {
Expand Down Expand Up @@ -78,29 +88,11 @@ impl Surface for BrowserSurface {
return reply;
}
match operation {
JevOperation::Click | JevOperation::Expand | JevOperation::Collapse => {
let reply = targeted("click", |target, _| Action::Click {
target,
new_tab: false,
});
let name = target.as_ref().and_then(|node| node.name.as_deref());
let reply = match (&reference, name) {
(Some(reference), name)
if covered(&reply) && (name.is_some() || sight::is_seen(reference)) =>
{
self.click_through_own_card(reference, name.unwrap_or_default())
.unwrap_or(reply)
}
_ => reply,
};
if reply.ok
&& let (Some(reference), Some(node)) = (&reference, &target)
&& selects_on_click(node)
&& sight::is_seen(reference)
{
self.select_if_ignored(reference);
}
reply
JevOperation::Click => self.press_element(target.as_ref(), reference.as_deref(), true),
// Opening or closing in place goes nowhere by design: never
// followed as a link that ignored its press.
JevOperation::Expand | JevOperation::Collapse => {
self.press_element(target.as_ref(), reference.as_deref(), false)
}
// Without a target the text goes where the focus is, as into an
// autocomplete's unnamed input once it has been opened — but
Expand Down Expand Up @@ -256,10 +248,16 @@ impl Surface for BrowserSurface {
fn navigate(&self, url: &str) -> DesktopResponse {
let page = self
.ensure_session()
.and_then(|id| self.block(self.browser.navigate(&id, NavigateRequest::new(url))));
.and_then(|id| self.block(self.browser.navigate(&id, NavigateRequest::new(url))))
.map(|page| (page.url, page.title));
// A heavy page can be read long before its `load` event fires.
let page = match page {
Err(error @ Error::Timeout { .. }) => self.drawn_page(url).ok_or(error),
other => other,
};
reply(
"navigate",
page.map(|page| json!({"url": page.url, "title": page.title})),
page.map(|(url, title)| json!({"url": url, "title": title})),
)
}

Expand Down
Loading
Loading