Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
c07d766
zgx: Activity, a sidebar view of what needs you
wingleeio Nov 6, 2026
63c24a3
proto, doc: a chat's wait on background work, and its sleep, on the s…
wingleeio Nov 6, 2026
8b31be7
engine, harness: a turn's end waits on the background work that will …
wingleeio Nov 6, 2026
ae4c54b
mcp: wait_on_background, and wait_for_turn waits through a chat's wait
wingleeio Nov 6, 2026
8c3f104
Waiting reaches every Rust client: the old desktop app's dot and labe…
wingleeio Nov 6, 2026
010da06
proto, zgx: one status rule for every viewport; zgx reads waits and s…
wingleeio Nov 6, 2026
25bddc7
zgx host: sessions.sleep, and chat.stop_waiting routed to the chat's …
wingleeio Nov 6, 2026
79528b8
zgx: Activity on the engine's waits and sleep; Woke, Custom sleep and…
wingleeio Nov 6, 2026
4cc19a0
engine: test that an agent marks only its own chat as waiting, and ne…
wingleeio Nov 6, 2026
d1ac343
zgx v2: its sidebar copy gains Activity too, recorded in step
wingleeio Nov 6, 2026
5852af0
zgx: the date picker in the app's own language
wingleeio Nov 6, 2026
82bc1ab
zgx: the date picker on one edge and one rhythm, compact; Sleep says …
wingleeio Nov 6, 2026
cbb8ab4
zgx: Waiting wears working's ring; Claude runs are told when to say t…
wingleeio Nov 6, 2026
566d271
Offer wait_on_background only where the runtime wakes its agent
wingleeio Nov 7, 2026
bb6b98b
Merge zeron/zgx into the Activity sidebar
wingleeio Nov 7, 2026
c0695e3
ui: test sessions carry no wait
wingleeio Nov 7, 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
2 changes: 2 additions & 0 deletions apps/zgx/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions apps/zgx/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ zeron-launch = { path = "../../crates/launch" }
zeron-notify = { path = "../../crates/notify" }
# (A message between chats: its routing header, read as the engine writes it.)
zeron-proto = { path = "../../crates/proto" }
# (Already in the build through zeron-proto: its times are chrono's, and its
# rows are read straight from the engine's JSON, not cloned first.)
chrono = "0.4"
serde = "1"
zeron-update = { path = "../../crates/update" }
tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] }
# Templates' theme files, JSON with comments, read for their previews
Expand Down
19 changes: 19 additions & 0 deletions apps/zgx/src/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,7 @@ pub fn install(_cx: &mut WindowContext, host: &ScriptHost) {
// A background process of the chat's stopped, on its host (the shipping
// app's `stop_background_task`): whether it's stopping.
host.async_action("chat.stop_task", move |args| async move { stop_task(args).await.map_err(String::from) });
host.async_action("chat.stop_waiting", move |id| async move { stop_waiting(text(&id)).await.map_err(String::from) });
let pick = which.clone();
host.async_action("chat.respond", move |args| {
let v = pick(&args);
Expand Down Expand Up @@ -2411,6 +2412,24 @@ async fn stop_task(args: Value) -> Result<Value, Outcome> {
Ok(Value::Bool(answer["stopping"].as_bool() == Some(true)))
}

/// The person's Stop waiting for a chat (`StopWaiting`), routed to its host:
/// what its agent said it waits on no longer holds it.
async fn stop_waiting(id: String) -> Result<Value, Outcome> {
if id.is_empty() {
return Err("missing");
}
let chats = CHATS.with(Rc::clone);
let mut params = json!({ "chatId": id });
if let Some(t) = chats.chat(&id).and_then(|c| chats.target(&c)) {
params["targetDeviceId"] = json!(t);
}
let answer = engine::get()
.call("StopWaiting", params)
.await
.map_err(|e| engine::outcome(&e))?;
Ok(Value::Bool(answer["stopped"].as_bool() == Some(true)))
}

async fn command(chat: &str, command: Json) -> Result<(), Outcome> {
engine::get()
.call("QueueCommand", json!({ "chatId": chat, "command": command }))
Expand Down
326 changes: 238 additions & 88 deletions apps/zgx/src/sessions.rs

Large diffs are not rendered by default.

24 changes: 23 additions & 1 deletion apps/zgx/ui/account/sessions.luau
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,18 @@
-- sessions.link(id) its Zeron conversation link: a promise
-- sessions.collapse(id, folded) a section of yours folded, everywhere
-- sessions.seen(id) its finished state seen, on every device
-- sessions.sleep(id, until?) asleep until `until` (Unix seconds), on every
-- device; none, awake (its host wakes it early when
-- it asks for you)
-- sessions.stop_waiting(id) what its agent said it waits on no longer
-- holds it
-- sessions.place(places) where sessions are put by hand, as the sidebar
-- has them now ({ pinned, sections })
--
-- A chat: { id, title, project?, device, branch?, cwd?, status?, updated,
-- created, harness?, agent?, model? }: `status` the sidebar's state
-- ("working", "input", "failed", "done"), nil idle; `updated` and `created`
-- ("working", "waiting" on background work that will wake it, "input",
-- "failed", "done"), nil idle; `updated` and `created`
-- Unix seconds; `agent` ds.Mark's name for its harness. A project: { id,
-- name, device, path, git, github? }. A device: { id, name, platform, here }.

Expand All @@ -57,7 +63,14 @@ export type Chat = {
running_subagents: number?,
side_chats: number?,
working_side_chats: number?,
-- What its agent waits on after its turn (status "waiting").
waiting: { WaitingOn }?,
-- Put to sleep: until when; or, woken and not looked at since, when it
-- woke (Unix seconds).
sleep_until: number?,
woke: number?,
}
export type WaitingOn = { id: string, kind: string, label: string }
-- A side chat: a chat under another (its `parent`).
export type Child = {
id: string,
Expand Down Expand Up @@ -174,6 +187,15 @@ function M.seen(id: string)
ui.invoke("sessions.seen", id)
end

function M.sleep(id: string, until_at: number?): any
-- (`until` is Luau's word: a key spelled out.)
return ui.call("sessions.sleep", { id = id, ["until"] = until_at })
end

function M.stop_waiting(id: string): any
return ui.call("chat.stop_waiting", id)
end

function M.place(places: Places)
ui.invoke("sessions.places", places)
end
Expand Down
1 change: 1 addition & 0 deletions apps/zgx/ui/assets/icons/ui/status-woke.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions apps/zgx/ui/date_picker_fixture.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
-- A date picker over a fixed March 2026, which starts on a Sunday, so its
-- days stand in its grid's first cells (`#dp-day-10` is March 10): chosen
-- the 10th at 10:00, nothing before the 5th at 9:20 to be had. What
-- tests/date_picker.test.luau presses on. (The moments are this Mac's
-- clock's, as the picker reads them.) `dp_demo` shows the gallery's page
-- instead (demos/date_picker.luau), to see that it builds.

ui.theme.strict("warn")
local ds = require("@ds")
ds.setup()
local DatePicker = require("@ds/date_picker")

-- A day's minute on this Mac's clock. (Luau's os.time reads its table as
-- UTC: the offset os.date finds there puts it right, looked up twice for a
-- moment near a change to or from summer time.)
local function at(y: number, m: number, d: number, h: number, mi: number): number
local wall = os.time({ year = y, month = m, day = d, hour = h, min = mi, sec = 0 })
local function offset(t: number): number
return os.time(os.date("*t", t) :: any) - t
end
return wall - offset(wall - offset(wall))
end

local value = ui.state("dp_value", at(2026, 3, 10, 10, 0) :: number?)
local changes = ui.state("dp_changes", 0)
local demo = ui.state("dp_demo", false)

return ui.column {
p = "$space.6",
gap = "$space.4",
bg = "$background",
ui.presence(demo, function(on: boolean): Node
return if on then require("@demos/date_picker").demo() else ui.div {}
end),
DatePicker {
id = "dp",
value = value,
min = at(2026, 3, 5, 9, 20),
on_change = function()
changes:set(changes:peek() + 1)
end,
},
}
57 changes: 57 additions & 0 deletions apps/zgx/ui/demos/date_picker.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
-- Date picker: a day and its time, from today on; a day alone; nothing
-- chosen yet.

local ds = require("@ds")
local kit = require("@demos/kit")

-- Tomorrow at 9 on this Mac's clock, to start from.
local function tomorrow_at_nine(): number
local d = os.date("*t", os.time()) :: any
local wall = os.time({ year = d.year, month = d.month, day = d.day + 1, hour = 9, min = 0, sec = 0 })
local function offset(t: number): number
return os.time(os.date("*t", t) :: any) - t
end
return wall - offset(wall - offset(wall))
end

local when = ui.state("dpk_when", tomorrow_at_nine() :: number?)
local day = ui.state("dpk_day", tomorrow_at_nine() :: number?)
local empty = ui.state("dpk_empty", nil :: number?)

-- What a picker holds, in words.
local function says(value: State<number?>): Node
return ds.Text {
function(): string
local v = value:get()
return if v then ds.Locale.exact(v) else "Nothing chosen"
end,
style = "caption",
tone = "tertiary",
}
end

return {
id = "date_picker",
title = "Date picker",
about = "A month of days and the time of day under it. Today's number is in the accent; the chosen day takes the menus' selected wash. Days and times before `min` can't be chosen. The arrows move between days, Page Up and Page Down a month.",
demo = function()
return kit.page(ui.row {
gap = "$space.8",
items = "start",
flex_wrap = true,
kit.block(
"FROM NOW ON, WITH ITS TIME",
ds.DatePicker {
id = "dpk-when",
value = when,
min = function(): number
return os.time()
end,
},
says(when)
),
kit.block("A DAY ALONE", ds.DatePicker { id = "dpk-day", value = day, time = false }, says(day)),
kit.block("NOTHING CHOSEN YET", ds.DatePicker { id = "dpk-empty", value = empty }, says(empty)),
})
end,
}
3 changes: 3 additions & 0 deletions apps/zgx/ui/demos/primitives.luau
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ return {
page("choices", "Select, slider, stepper", function()
return require("@demos/choices")
end),
page("date_picker", "Date picker", function()
return require("@demos/date_picker")
end),
page("tabs", "Tabs", function()
return require("@demos/tabs")
end),
Expand Down
Loading
Loading