Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
608 changes: 595 additions & 13 deletions Cargo.lock

Large diffs are not rendered by default.

17 changes: 12 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ members = [
"crates/l0-chat",
"crates/oauth-service",
"crates/markdown-editor",
"crates/wasm-host",
"apps/reference",
"apps/news/host-service",
"apps/mail/host-service",
Expand All @@ -49,6 +50,7 @@ exclude = [
"apps/appcard/tools",
"apps/appcard/vendor",
"apps/appcard/app/splash-native",
"apps/wasmlab/guest",
]
default-members = ["desktop"]

Expand All @@ -71,7 +73,7 @@ octosense-reference = { path = "apps/reference" }
makepad-sheets = { git = "https://github.com/OctoSense-org/makepad.git", rev = "32d6415fb7476345ad36ee4f98d6f844d1f07fd2", default-features = false }
makepad-terminal = { git = "https://github.com/OctoSense-org/makepad.git", rev = "32d6415fb7476345ad36ee4f98d6f844d1f07fd2" }
octosense-appcard = { path = "apps/appcard/module" }
octosense-app-hub-app = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "b1109a397ae45349546d50889a1706f6c58fb923" }
octosense-app-hub-app = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "2e5768964f15916cdb66cc6824f7671dafc6445a" }
makepad-calculator = { git = "https://github.com/OctoSense-org/makepad.git", rev = "32d6415fb7476345ad36ee4f98d6f844d1f07fd2", default-features = false }
makepad-clock = { git = "https://github.com/OctoSense-org/makepad.git", rev = "32d6415fb7476345ad36ee4f98d6f844d1f07fd2", default-features = false }
makepad-notes = { git = "https://github.com/OctoSense-org/makepad.git", rev = "32d6415fb7476345ad36ee4f98d6f844d1f07fd2", default-features = false }
Expand Down Expand Up @@ -106,17 +108,17 @@ octoscript-makepad = { git = "https://github.com/OctoSense-org/Octoscript-Makepa
octoscript-node = { git = "https://github.com/OctoSense-org/Octoscript-Makepad.git", rev = "33dea2f1f3ad3f1346a219aa8cf6e91b31361e23" }
# --- App Hub's host service registry the Mail and llm services register
# into; the same revision as App Hub's app crate (a native app, above).
octosense-appstore = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "b1109a397ae45349546d50889a1706f6c58fb923" }
octosense-appstore = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "2e5768964f15916cdb66cc6824f7671dafc6445a" }
# The app policy (resolution, isolate settings, the Splash adapter) and the
# Hub client the Card runner resolves an app's policy with; the shell's
# glance tiles run cards under it (crates/shell/src/glance_card.rs).
octosense-app-policy = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "b1109a397ae45349546d50889a1706f6c58fb923" }
octosense-app-hub = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "b1109a397ae45349546d50889a1706f6c58fb923" }
octosense-app-policy = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "2e5768964f15916cdb66cc6824f7671dafc6445a" }
octosense-app-hub = { git = "https://github.com/OctoSense-org/OctoSense-App-Hub", rev = "2e5768964f15916cdb66cc6824f7671dafc6445a" }
# The app contract (ADR 0005), from crates.io by version: App Hub, the shell
# and Rinx all depend on `octosense-app-contract = "1"`, so Cargo links ONE
# 1.x for the whole build (Cargo.lock pins the exact version OctoSense ships)
# and App Hub and Rinx no longer need the same App Hub commit.
octosense-app-contract = "1.6"
octosense-app-contract = "1.7"
# --- octos: ONE source for every octos crate (reviewed octos#2715 at
# b0759a57: bounded ledger session-directory names, serialized peer purge
# against session writes, and provider transport diagnostics that omit
Expand Down Expand Up @@ -165,6 +167,7 @@ octosense-toolbox = { path = "crates/toolbox" }
octosense-l0-chat = { path = "crates/l0-chat" }
octosense-oauth-service = { path = "crates/oauth-service" }
octosense-markdown-editor = { path = "crates/markdown-editor" }
octosense-wasm-host = { path = "crates/wasm-host" }
article-core = { git = "https://github.com/hagency-org/Rinx.git", tag = "v1.1.0" }
article-makepad = { git = "https://github.com/hagency-org/Rinx.git", tag = "v1.1.0" }
octosense-llm-config = { path = "apps/ai-providers/config" }
Expand Down Expand Up @@ -194,6 +197,10 @@ keyring = "3"
# process with them (OctoSense hosting Rinx).
rusqlite = { version = "0.40.1", features = ["bundled"] }
log = "0.4"
# Developers' own functions compiled to WebAssembly, run for their app with
# a deadline and a memory cap (crates/wasm-host, ADR 0011). Cranelift compiles;
# no component model, async, GC or threads.
wasmtime = { version = "49.0.2", default-features = false, features = ["cranelift", "runtime", "std", "parallel-compilation"] }

# Workspace-wide clippy policy for the crates that opt in (`[lints]
# workspace = true`, AppCard's): lints that are real but tracked elsewhere.
Expand Down
15 changes: 15 additions & 0 deletions apps/wasmlab/bundle/AGENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Wasm Lab

Wasm Lab's tools are its own Rust functions, run on this device in a sandbox:
they compute an answer from what you give them and reach nothing else (no
files, network, calendar or other app).

- wasmlab.find_slots: free slots of a given length in a day, around busy
intervals you pass ("HH:MM" pairs). It does not read anyone's calendar;
get the busy times first, from the person or from a tool that has them.
- wasmlab.rank: rank names against a query, best first, tolerating typos.
- wasmlab.diff: a unified line diff of two texts, with added and removed
counts.

Report what a tool returned. When a tool says the input is wrong (a bad time,
a day that ends before it starts), fix the input or ask; do not guess.
Binary file added apps/wasmlab/bundle/fns/wasmlab.wasm
Binary file not shown.
1 change: 1 addition & 0 deletions apps/wasmlab/bundle/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
257 changes: 257 additions & 0 deletions apps/wasmlab/bundle/main.splash
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
// BEGIN shared app interface
// Shared system-app interface. Run tools/sync-app-interface.py after editing.
// Bundles carry this prelude so they work in the contained standalone host too.
// Resolve the current theme after a host restyle, not a retained prelude alias.
let theme = mod.theme
let ui_ink = theme.color_text
let ui_page = theme.color_bg_app
let ui_light = dot(ui_ink, #x36b61200) < 0.5
let ui_surface = if ui_light { #xffffff } else { mix(ui_page, ui_ink, 0.06) }
let ui_field = mix(ui_page, ui_ink, 0.075)
let ui_hover = mix(ui_page, ui_ink, 0.12)
let ui_border = mix(ui_page, ui_ink, 0.22)
let ui_muted = mix(ui_ink, ui_page, 0.24)
let ui_link = mix(ui_ink, #x356bce, 0.25)
let ui_primary = #x285cd6
let ui_success = mix(ui_ink, #x28a55b, 0.20)
let ui_danger = mix(ui_ink, #xe13b53, 0.20)
let UiButton = ButtonFlat{height: 44 margin: 0 padding: Inset{left: 14 right: 14}
draw_bg +: {border_radius: 12.0 border_size: 0.0 color: #x00000000 color_hover: ui_hover color_down: ui_field color_focus: ui_hover}
draw_text +: {color: ui_link color_hover: ui_link color_down: ui_link color_focus: ui_link text_style: theme.font_bold{font_size: 14}}
}
let UiPrimary = UiButton{
draw_bg +: {color: ui_primary color_hover: #x3468df color_down: #x204cb8 color_focus: #x204cb8}
draw_text +: {color: #xffffff color_hover: #xffffff color_down: #xffffff color_focus: #xffffff}
}
let UiPill = UiButton{draw_bg +: {color: ui_surface border_radius: 22.0} draw_text +: {color: ui_ink color_hover: ui_ink color_down: ui_ink color_focus: ui_ink}}
let UiField = TextInput{width: Fill height: 48 margin: 0 padding: Inset{left: 14 right: 14 top: 12 bottom: 12}
draw_bg +: {color: ui_field color_hover: ui_field color_focus: ui_field color_empty: ui_field
border_color: ui_border border_color_hover: ui_link border_color_focus: ui_link border_color_empty: ui_border border_radius: 12.0}
draw_text +: {color: ui_ink color_hover: ui_ink color_focus: ui_ink color_empty: ui_muted color_empty_hover: ui_muted color_empty_focus: ui_muted text_style: theme.font_regular{font_size: 15}}
draw_selection.color: #x557dcc55
}
let UiCard = RoundedView{width: Fill height: Fit flow: Down spacing: 12 padding: 20
show_bg: true draw_bg +: {color: ui_surface border_radius: 16.0}}
let UiCaption = Label{padding: 0 flow: Flow.Right{wrap: true} width: Fill draw_text +: {color: ui_muted text_style: theme.font_regular{font_size: 13 line_spacing: 1.4}}}
let UiTitle = Label{padding: 0 flow: Flow.Right{wrap: true} width: Fill draw_text +: {color: ui_ink text_style: theme.font_bold{font_size: 28 line_spacing: 1.2}}}
// END shared app interface
// Wasm Lab: Rust functions that ship in this bundle as
// fns/wasmlab.wasm (built from apps/wasmlab/guest), run by the shell's
// `wasm` service in a sandbox with a deadline and a memory cap. Each card
// calls one function through host.request and shows what came back and the
// round trip, as this script sees it.
let ground = ui_page
let ink = ui_ink
let secondary = ui_muted

let APPS = ["Calendar" "Calculator" "Camera" "Clock" "Contacts" "Files" "Mail" "Maps" "Messages" "Music"
"News" "Notes" "Phone" "Photos" "Reminders" "Settings" "Weather" "YouTube" "Terminal" "App Hub"
"Sheets" "Wallet" "Health" "Translate" "Podcasts" "Books" "Voice Memos" "Compass" "Measure" "Stocks"
"Home" "Find My" "Shortcuts" "Magnifier" "Tips" "Fitness" "Recorder" "Gallery" "Browser" "Assistant"]
// Commas between the pairs: `] [` would read as indexing.
let BUSY = [["09:00" "09:30"], ["10:00" "11:00"], ["12:00" "13:00"], ["14:30" "15:00"], ["16:00" "17:30"]]
let OLD = "milk\neggs\nbread\nbutter\napples\ncoffee\n"
let NEW = "milk\neggs\nsourdough bread\nbutter\napples\npears\ncoffee\n"
let SAMPLE = "# Weekly notes\n\nSome *emphasis*, `code` and a [link](https://example.org).\n\n- [x] ship the MVP\n- [ ] measure on the phone\n\n| function | input |\n|---|---|\n| find_slots | busy times |\n| fuzzy_rank | names |\n\n> Quoted, with **bold** inside.\n\n"

let ranked = []
let rank_note = ""
let rank_seq = 0
let duration = 30
let slots = []
let slots_note = ""
let diff = nil
let diff_note = ""
let md_note = ""
let md_head = ""
let rogue_note = ""
let runtime = nil
let runtime_note = ""

// Milliseconds to one decimal.
fn ms(t0){ "" + floor((time_now() - t0) * 10000.0 + 0.5) / 10 + " ms" }

fn rank(text){
rank_seq = rank_seq + 1
let seq = rank_seq
let t0 = time_now()
host.request("wasm.fuzzy_rank", {query: text items: APPS limit: 8}, fn(r){
if seq != rank_seq { return }
if r.is_ok {
ranked = r.data.ranked
let noun = if ranked.len() == 1 { " match" } else { " matches" }
rank_note = ranked.len() + noun + " in " + ms(t0)
}
else { ranked = [] rank_note = r.error }
ui.rank_list.render()
})
}

fn find_slots(minutes){
duration = minutes
let t0 = time_now()
host.request("wasm.find_slots", {day_start: "08:30" day_end: "18:00" duration: minutes busy: BUSY max: 12}, fn(r){
if r.is_ok { slots = r.data.slots slots_note = slots.len() + " free slots of " + minutes + " min in " + ms(t0) }
else { slots = [] slots_note = r.error }
ui.durations.render()
ui.slot_list.render()
})
}

fn compare(){
let t0 = time_now()
host.request("wasm.text_diff", {old: OLD new: NEW context: 1}, fn(r){
if r.is_ok { diff = r.data diff_note = "+" + diff.added + " −" + diff.removed + " in " + ms(t0) }
else { diff = nil diff_note = r.error }
ui.diff_view.render()
})
}

fn render_md(copies){
let text = ""
for i in copies { text = text + SAMPLE }
let t0 = time_now()
host.request("wasm.md_to_html", text, fn(r){
if r.is_ok {
md_note = floor(text.len() / 100) / 10 + " KB of Markdown → " + floor(r.data.text.len() / 100) / 10 + " KB of HTML in " + ms(t0)
md_head = r.data.text.split("\n")[0]
} else { md_note = r.error md_head = "" }
ui.md_view.render()
})
}

fn misbehave(mode){
rogue_note = "Running “" + mode + "”…"
ui.rogue_view.render()
let t0 = time_now()
host.request("wasm.rogue", {mode: mode}, fn(r){
if r.is_ok { rogue_note = mode + ": returned (unexpected)" }
else { rogue_note = mode + ": " + r.error + " (after " + ms(t0) + ")" }
ui.rogue_view.render()
// The next call gets a fresh instance.
rank(ui.query.text())
load_runtime()
})
}

fn load_runtime(){
let t0 = time_now()
host.request("wasm.functions", {}, fn(r){
if r.is_ok { runtime = r.data runtime_note = "Answered in " + ms(t0) }
else { runtime = nil runtime_note = r.error }
ui.runtime_view.render()
})
}

fn module_line(m){
let how = if m.from_cache { "loaded compiled code from the cache" } else { "compiled" }
m.file + ": " + floor(m.bytes / 1024) + " KiB, " + how + " in " + m.load_ms + " ms; memory " + floor(m.memory_bytes / 1048576) + " MiB; renewed " + m.renewed + "×"
}

// A missing key reads as `fallback` (as News and Photos do it).
fn optional(it, key, fallback){
let value = try { it[key] } catch { nil }
if value == nil { return fallback }
value
}

fn stats_line(name){
let s = optional(runtime.stats, name, nil)
if s == nil { return name + ": not called yet" }
name + ": " + s.calls + " calls, mean " + s.mean_us + " µs, max " + s.max_us + " µs, " + s.errors + " errors"
}

fn boot(){
load_runtime()
rank("cal")
find_slots(30)
compare()
render_md(1)
}
start_timeout(0.05, || boot())

let Section = Label{width: Fill text: "" draw_text.color: ink draw_text.text_style: theme.font_bold{font_size: 16}}
let Note = UiCaption{text: ""}
let Line = Label{width: Fill text: "" draw_text.color: ink draw_text.text_style.font_size: 14}
let Code = Label{width: Fill text: "" flow: Flow.Right{wrap: true} draw_text.color: ink draw_text.text_style: theme.font_code{font_size: 12}}
let Choice = UiPill{}
let ChoiceOn = UiPill{
draw_bg +: {color: ink color_hover: ink color_down: ink}
draw_text +: {color: ground color_hover: ground color_down: ground}
}

SolidView{width: Fill height: Fill flow: Overlay align: Align{x: 0.5} show_bg: true draw_bg.color: ground
ScrollYView{width: Fill max_width: 760 height: Fill flow: Down spacing: 14 padding: Inset{left: 14 right: 14 top: 16 bottom: 32}
UiTitle{text: "Wasm Lab"}
UiCaption{text: "Rust functions shipped in this app, run by the shell in a sandbox: no files, network or other apps, a 2 s deadline and a 256 MiB memory cap."}

UiCard{
Section{text: "Find an app"}
query := UiField{empty_text: "Type part of a name" on_change: |text| rank(text)}
rank_list := View{width: Fill height: Fit flow: Down spacing: 6 on_render: || {
Note{text: rank_note}
for it in ranked { Line{text: it.item + " " + it.score} }
}}
}

UiCard{
Section{text: "Free time today"}
UiCaption{text: "Busy 09:00–09:30, 10:00–11:00, 12:00–13:00, 14:30–15:00, 16:00–17:30; the day runs 08:30–18:00."}
durations := View{width: Fill height: Fit flow: Flow.Right{wrap: true} spacing: 8 on_render: || {
for m in [15 30 60 90] {
if m == duration { ChoiceOn{text: m + " min" on_click: || find_slots(m)} }
else { Choice{text: m + " min" on_click: || find_slots(m)} }
}
}}
slot_list := View{width: Fill height: Fit flow: Down spacing: 6 on_render: || {
Note{text: slots_note}
for s in slots { Line{text: s[0] + " – " + s[1]} }
}}
}

UiCard{
Section{text: "What changed in the list"}
diff_view := View{width: Fill height: Fit flow: Down spacing: 6 on_render: || {
Note{text: diff_note}
if diff != nil { Code{text: diff.unified} }
}}
UiButton{text: "Compare again" on_click: || compare()}
}

UiCard{
Section{text: "Markdown to HTML"}
md_view := View{width: Fill height: Fit flow: Down spacing: 6 on_render: || {
Note{text: md_note}
Code{text: md_head}
}}
View{width: Fill height: Fit flow: Right spacing: 8
UiButton{text: "Render the sample" on_click: || render_md(1)}
UiButton{text: "Render it ×40" on_click: || render_md(40)}
}
}

UiCard{
Section{text: "Misbehave"}
UiCaption{text: "Each ends in an error, never a crash; the next call gets a fresh instance."}
View{width: Fill height: Fit flow: Flow.Right{wrap: true} spacing: 8
UiButton{text: "Loop forever" on_click: || misbehave("loop")}
UiButton{text: "Allocate endlessly" on_click: || misbehave("alloc")}
UiButton{text: "Panic" on_click: || misbehave("panic")}
UiButton{text: "Recurse" on_click: || misbehave("recurse")}
}
rogue_view := View{width: Fill height: Fit flow: Down on_render: || { Note{text: rogue_note} }}
}

UiCard{
Section{text: "Runtime"}
runtime_view := View{width: Fill height: Fit flow: Down spacing: 6 on_render: || {
Note{text: runtime_note}
if runtime != nil {
for m in runtime.modules { Line{text: module_line(m)} }
for name in runtime.functions { Line{text: stats_line(name)} }
}
}}
UiButton{text: "Refresh" on_click: || load_runtime()}
}
}
}
14 changes: 14 additions & 0 deletions apps/wasmlab/bundle/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"schema": 1,
"id": "os.wasmlab",
"version": "0.1.0",
"name": "Wasm Lab",
"integrity": { "bundle_blake3": "" },
"capabilities": ["wasm"],
"agent": {
"profile": "read-only",
"tools": ["ask_user_question"],
"model": { "needs": ["tool_calling"] },
"instructions": "AGENT.md"
}
}
Loading
Loading