diff --git a/frontend/packages/swarm-ui/src/pages/CreateAgentPage.css b/frontend/packages/swarm-ui/src/pages/CreateAgentPage.css index b1fe91a2..394ad05c 100644 --- a/frontend/packages/swarm-ui/src/pages/CreateAgentPage.css +++ b/frontend/packages/swarm-ui/src/pages/CreateAgentPage.css @@ -1,45 +1,10 @@ /* — the create-agent form. Input/button chrome now comes from the shared `ui/` form kit (`TextField`/`SelectField`/ `Button`); this file only owns the page's own layout + copy. Column, - not row inside the form itself: with a second field (hive) added, a - row layout put fields of different natural widths on one baseline and - looked misaligned — mara: "make it a col". Reuses the same base16 - slots (../theme.css) every other component draws from. - - This layout dropped an explicit page `max-width` after a few rounds: - first added so bare `Panel` (no width opinion of its own) wouldn't - fill `.shell-body`'s full 60em column behind a narrow form (the - original "weird empty space" complaint), then widened for two panels - side by side — but once both were equal-width flex children that cap - itself became "doesn't fill the space" (mara: "the cards are neither - centered nor fill the space tho"). Removing it lets the row fill - `.shell-body`'s own width like most pages — nothing sits off-center - once nothing is narrower than the row. - - `.create-agent-form-col`/`.create-agent-info-col` are equal-sized - (`flex: 1 1 0` — zero basis so the 1.5em `gap` splits the row evenly, - not each column keeping its natural content width) per mara's "both - cards should be equal sized". `min-width` is the wrap threshold below - which the row goes single-column instead (info card moving under the - form) — no separate media query needed. Inputs fill their whole - column now too ("inputs should fill full card width") — the kit's - `.ui-form-control`/`.ui-form-field` dropped their old 16em cap for - exactly this (../ui/form-field/FormField.css). */ -.create-agent-layout { - display: flex; - flex-wrap: wrap; - align-items: flex-start; - gap: 1.5em; -} -.create-agent-form-col, -.create-agent-info-col { - flex: 1 1 0; - min-width: 16em; -} -.create-agent-info-glyph { - margin: 0 0 0.5em; - font-size: 2em; -} + not row: with a second field (hive) added, a row layout put fields of + different natural widths on one baseline and looked misaligned — + mara: "make it a col". Reuses the same base16 slots (../theme.css) + every other component draws from. */ .create-agent-intro { margin: 0 0 1.5em; color: var(--muted); diff --git a/frontend/packages/swarm-ui/src/pages/CreateAgentPage.tsx b/frontend/packages/swarm-ui/src/pages/CreateAgentPage.tsx index eaa22152..7bb605b2 100644 --- a/frontend/packages/swarm-ui/src/pages/CreateAgentPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/CreateAgentPage.tsx @@ -125,74 +125,44 @@ export function CreateAgentPage() { } return ( -
-
- -

- Create a new agent's swarm-level identity. This only queues the job — check{' '} - jobs to watch it settle. -

- {hivesError && ( - - )} -
- - - - - {result.status === 'done' && ( -

- queued as job node #{result.nodeId} — watch it in jobs -

- )} - {result.status === 'error' && ( - - )} -
-
-
- {/* Describes the intended end state (a running agent), not today's - literal behaviour (deploying the container onto the hive isn't - wired up server-side yet — see this file's top comment) — - mara's explicit call on this copy: read as finished, not as a - running commentary on partial implementation. */} - - -

- Submitting this queues everything a new agent needs: a swarm-level identity, a config - repo on the forge with the operator added as a collaborator, and a container running - the agent on the hive you pick above. Watch it all settle on{' '} - jobs. -

-
-
-
+ +

+ Create a new agent's swarm-level identity. This only queues the job — check{' '} + jobs to watch it settle. +

+ {hivesError && } +
+ + + + + {result.status === 'done' && ( +

+ queued as job node #{result.nodeId} — watch it in jobs +

+ )} + {result.status === 'error' && ( + + )} +
); } diff --git a/frontend/packages/swarm-ui/src/ui/form-field/FormField.css b/frontend/packages/swarm-ui/src/ui/form-field/FormField.css index ed4f1393..29257bdd 100644 --- a/frontend/packages/swarm-ui/src/ui/form-field/FormField.css +++ b/frontend/packages/swarm-ui/src/ui/form-field/FormField.css @@ -1,34 +1,18 @@ /* — label stacked above its control, plus the shared `.ui-form-control` chrome every text/select input in the kit draws - from (one class, so the two never drift). `width: 100%` fills - whichever container the caller gives it — the kit itself has no - opinion on a maximum width; a page that wants one narrower than its - own layout caps it at the layout level (`CreateAgentPage.css`'s - `.create-agent-form-col` is the existing example), same reasoning - `Panel` has no width opinion of its own either. `min-height` is a - touch-target floor (44px at the default 16px root font — WCAG - 2.5.5's minimum), not a visual choice — it's the same on every - control in the kit whether or not it's ever used on a touch device, - since the alternative is a component that behaves differently per - input method. Colours are the shared base16-derived vars - (../../theme.css), never literal. - - The field wrapper repeats `.ui-form-control`'s own `width: 100%` - rather than leaving the wrapper unconstrained: inside a shrink-to-fit - flex column (`CreateAgentPage`'s form is one), an unconstrained - wrapper sizes to its own content — and a `width: 100%` *control* - inside an auto-width wrapper resolves against that shrunk width, not - the container the page actually gave it, so two fields with - differently-long labels ("agent name" vs "hive") ended up with - differently-wide inputs — the misalignment mara reported on the - create-agent page. Matching the two declarations here means every - field's control width is driven by the same container width - regardless of its label's length or its siblings'. */ + from (one class, so the two never drift). `max-width` + `width: 100%` + rather than a fixed `width`: caps the control on a wide desktop + viewport without forcing an overflow on a narrow/touch one. + `min-height` is a touch-target floor (44px at the default 16px root + font — WCAG 2.5.5's minimum), not a visual choice — it's the same on + every control in the kit whether or not it's ever used on a touch + device, since the alternative is a component that behaves differently + per input method. Colours are the shared base16-derived vars + (../../theme.css), never literal. */ .ui-form-field { display: flex; flex-direction: column; gap: 0.3em; - width: 100%; } .ui-form-field-label { font-size: 0.85em; @@ -42,6 +26,7 @@ padding: 0.4em 0.6em; font: inherit; width: 100%; + max-width: 16em; box-sizing: border-box; min-height: 2.75em; } diff --git a/hive-matrix-mcp/src/handlers.rs b/hive-matrix-mcp/src/handlers.rs index 7ec2059f..1dc1e724 100644 --- a/hive-matrix-mcp/src/handlers.rs +++ b/hive-matrix-mcp/src/handlers.rs @@ -193,52 +193,24 @@ async fn unread_guard(client: &Client, room: &matrix_sdk::Room) -> Option Vec { +) -> Option<(OwnedEventId, OwnedUserId, String)> { use matrix_sdk::ruma::api::Direction; use matrix_sdk::ruma::api::client::message::get_message_events; let mut req = get_message_events::v3::Request::new(room.room_id().to_owned(), Direction::Backward); - req.limit = matrix_sdk::ruma::UInt::from(UNREAD_LOOKBACK); - let Ok(resp) = client.send(req).await else { - return Vec::new(); - }; - resp.chunk - .iter() - .filter_map(|raw| raw.deserialize().ok()) - .collect() -} - -/// Whether `event` carries actor intent, as opposed to noise nobody -/// "did" anything to produce. Everything counts by default — messages -/// (of every msgtype, not just `m.room.message`'s common ones), -/// reactions, state changes (join/leave/topic/name/etc. are all real -/// activity an agent should see, mara: don't lump them in with -/// housekeeping noise). The one deliberate exception is `m.space.child`: -/// a periodic re-apply of an *unchanged* value still appends a timeline -/// event (see [`room_unread_state`]'s doc comment), and that specific -/// redundant re-emit is the one thing this function exists to filter — -/// an earlier version of this filter over-corrected by excluding whole -/// event categories instead of just that one type; narrower is right. -fn is_intentional(event: &matrix_sdk::ruma::events::AnyTimelineEvent) -> bool { - use matrix_sdk::ruma::events::{AnyStateEvent, AnyTimelineEvent}; - !matches!(event, AnyTimelineEvent::State(AnyStateEvent::SpaceChild(_))) + req.limit = matrix_sdk::ruma::UInt::from(1u32); + let resp = client.send(req).await.ok()?; + let raw = resp.chunk.first()?; + let ev = raw.deserialize().ok()?; + let body = extract_body(&ev); + Some((ev.event_id().to_owned(), ev.sender().to_owned(), body)) } /// Whether `room` carries content the agent hasn't caught up on. @@ -260,22 +232,21 @@ fn is_intentional(event: &matrix_sdk::ruma::events::AnyTimelineEvent) -> bool { /// Self-authored latest events are never "unread" — after a daemon /// rebuild the read receipt can lag behind the agent's own just-sent /// message, which must not self-wake it. -/// -/// Walks backward through up to [`UNREAD_LOOKBACK`] events (not just the -/// newest one) looking for the first [`is_intentional`] hit, skipping a -/// chatty state re-emit nobody "posted" instead of treating it as gospel. -/// See the three branches below for what each outcome means. async fn room_unread_state( client: &Client, room: &matrix_sdk::Room, ) -> Option<(OwnedEventId, OwnedUserId, String)> { let own_user_id = client.user_id()?; + let (event_id, sender, body) = latest_event(client, room).await?; + if sender.as_str() == own_user_id.as_str() { + return None; + } // `load_user_receipt` takes `ruma::events::receipt::ReceiptType` // (imported here as `LocalReceiptType`), a distinct type from the // `ReceiptType` this module already imports for `send_single_receipt` // (`ruma::api::client::receipt::create_receipt::v3::ReceiptType`) — two // same-named enums from different ruma crates, not interchangeable. - let read_event_id = room + let read = room .load_user_receipt( LocalReceiptType::Read, ReceiptThread::Unthreaded, @@ -283,47 +254,11 @@ async fn room_unread_state( ) .await .ok() - .flatten() - .map(|(id, _)| id); - - let events = recent_events(client, room).await; - for event in &events { - if read_event_id.as_deref() == Some(event.event_id()) { - // Scanned back to the agent's own read receipt without - // hitting anything intentional first — proven read, not - // guessed: there cannot be an unread intentional event in a - // range we've fully walked. - return None; - } - if !is_intentional(event) { - continue; - } - if event.sender().as_str() == own_user_id.as_str() { - return None; - } - return Some(( - event.event_id().to_owned(), - event.sender().to_owned(), - extract_body(event), - )); - } - // Exhausted the lookback window without hitting the receipt or an - // intentional event — genuinely unknown (the receipt is further - // back than we scanned). Falls back to the pre-fix behaviour - // (surface the newest event, whatever its type) rather than - // guessing "read": a false "unread" just costs a wasted wake, a - // false "read" risks silently swallowing a real message. Expected - // to be rare now that the state-churn source this fix targets is - // itself bounded. - let newest = events.first()?; - if newest.sender().as_str() == own_user_id.as_str() { + .flatten(); + if read.is_some_and(|(id, _)| id == event_id) { return None; } - Some(( - newest.event_id().to_owned(), - newest.sender().to_owned(), - extract_body(newest), - )) + Some((event_id, sender, body)) } pub async fn send_message(client: &Client, room_ref: &str, body: &str) -> DaemonResponse {