Commit Graph
695 Commits
Author SHA1 Message Date
Michael Neale 884dfa59ad fix(serverless): revert useQueryClient in useWorkspaceInit (crashed window)
useWorkspaceInit runs above the QueryClientProvider, so useQueryClient()
threw on mount and the window never showed. Cross-workspace cache isolation
is already handled by WorkspaceQueryProvider being keyed on workspaceKey
(fresh QueryClient per workspace via remount).
2026-06-02 17:17:13 +10:00
Michael Neale 10b8f06463 fix(serverless): clear React Query cache on workspace switch
Channel lists, message timelines, members, and agent lists are keyed in
React Query without the workspace id, so switching workspaces bled the
previous workspace's channels/data into the new one. resetWorkspaceState()
now calls queryClient.clear() alongside the existing module-singleton
resets, per the AGENTS.md workspace-switch contract.
2026-06-02 17:05:57 +10:00
Michael Neale 5afc0e0266 fix(serverless): agent subscribes to ALL relays + dedup (damus RelayPool pattern)
The agent connected to a single relay, so a message stored only on relay B
(because relay A rate-limited the write) was invisible to it — the split-brain.
Relays don't gossip (verified against damus RelayPool.swift, NIP-01/65/17,
nostr-tools SimplePool): clients must publish-to-many AND subscribe-to-many +
dedup by event id. The harness now spawns one background task per relay sharing
one event stream, fans out commands to all, and dedups in next_event() via a
bounded seen-id ring (like damus seenEvents).

Proven live: agent on [damus,nos.lol] receives a kind-9 published ONLY to
nos.lol (agent_multi_relay_subscribe_no_split_brain), plus the existing
encrypted-receive test now runs against a comma relay list.
2026-06-02 16:56:56 +10:00
Michael Neale bc92b752e0 fix(serverless): agent re-discovers channels (no 44100 push) + split comma relay list
The sprout-acp harness is single-relay but serverless workspaces pass a
comma-separated relay list, causing a DNS crash on the raw string. Split to the
first relay (matching the desktop live-WS convention).

Also: a generic public relay never emits the kind:44100 'member added'
notification the agent relies on to learn it was added to a new channel (that's
a Sprout-relay side effect). In serverless mode, periodically re-run
discover_channels (kind:39002 #p=agent) and subscribe to newly-joined channels.
Without this, a channel created/joined after the agent started is never watched.

Live test now passes a comma-separated relay list through connect_with_mode.
2026-06-02 16:39:07 +10:00
Michael Neale 0b679ccfb1 fix(serverless): agent connects to first relay of comma list (DNS crash)
The ACP harness is single-relay but serverless workspaces pass a
comma-separated relay LIST (wss://a,wss://b,...). connect_with_mode fed
the raw comma string to connect_async, which failed DNS lookup
("nodename nor servname provided") and crashed the agent before it ever
subscribed — so it never saw mentions and never responded.

Split to the first (primary) relay, matching the desktop live-WS
convention. Regression-covered by the live agent test now using a
comma-separated RELAY_URL.
2026-06-02 15:43:31 +10:00
Michael Neale 51c89d4e64 fix(serverless): standard multi-relay Nostr — fix split-brain reads, query flood, single-relay live subscription
The serverless transport was behaving like it talked to the bespoke Sprout
server instead of a standard multi-relay Nostr client. Three real bugs:

1. Split-brain: messages were sent over the multi-relay pool (landing on
   whichever relay accepts, e.g. nos.lol when damus rate-limits) but READ over
   a single-relay live WS (damus only) — so your own message was invisible.
   Fix: in serverless, send + history + LIVE SUBSCRIPTION all go through the
   Rust multi-relay pool. New persistent pool subscribe()/unsubscribe() keeps a
   REQ open on ALL relays at once and merges+dedups (standard Nostr, like damus
   SimplePool). New commands: query_channel_messages, subscribe/unsubscribe_
   channel_messages; events streamed to the UI via serverless-event:<channel>.

2. Query flood: get_channels ran {kinds:[39000],limit:5000} (discover ALL
   network channels) + a 500-id member-count batch on a public relay. Fix:
   serverless skips network-wide discovery; only your member channels are
   listed (4 scoped queries).

3. Diagnostics: added [serverless]/[pool] backend logging and [relay] webview
   console logging so failures are visible, not silent.

Proven with live tests against damus+nos.lol: multi-relay publish survives
per-relay rate-limit, and the live subscription delivers a message published
to a different relay than the naive primary (the split-brain fix).
2026-06-02 15:35:28 +10:00
Michael Neale bf6be68d79 fix(serverless): persistent connection pool — stop rate-limit storm
Root cause of 'everything is rate-limited' / 'noting too much': the
serverless transport opened a FRESH WebSocket per query and per publish
(connect→send→close). get_channels fires ~10 queries, create channel 2
publishes, add-agent several more — each a new connection. Public relays
(damus, nos.lol) aggressively rate-limit connection storms + event
bursts, so every action failed.

Fix (validated against damus/nostr-tools idiomatic patterns + NIP-01/65):
- ws_pool.rs: one persistent WebSocket per relay, reused for all
  queries/publishes; background reader dispatches by sub/event id;
  transparent reconnect; NIP-42 AUTH answered on the same socket.
- Don't block on slow relays: publish returns on FIRST acceptance
  (FuturesUnordered); connect has a 4s timeout; failed relays enter a
  30s cooldown so a dead relay isn't re-dialed every query.
- Multi-relay publish (success if any accepts) confirmed idiomatic —
  not a mistake; it's the resilience model and hides per-relay rate caps.
- Pool cleared on workspace switch.

Verified live: 5-relay burst (15 queries + 6 publishes) that previously
hung 3+min now completes in 17s with zero rate-limits; create→join
roundtrip passes on 2 relays. 426 unit tests pass, clippy clean.

New live test: serverless_burst_no_rate_limit.
2026-06-02 15:05:25 +10:00
Michael Neale 8e8f93a85b fix(serverless): local-only agents, rate-limit retry, channel delete
- list_relay_agents returns empty in serverless: managed agents are local
  desktop subprocesses, not a relay registry. Fixes 'agent parse failed:
  missing field name' from foreign kind-10100 events on public relays.
- submit_event_ws retries once on all-relays-rate-limited (public relays
  throttle the write burst when adding an agent). Succeeds if any relay
  accepts. Fixes 'Failed to add goose: rate-limited'.
- delete_channel publishes NIP-09 (kind 5) deletion over the 39000/39002
  addressable coordinates in serverless mode (the kind-9008 command is a
  no-op on a generic relay). Owner-only.
- Tests + file-size exception.
2026-06-02 14:37:02 +10:00
Michael Neale 6b756dae1e fix(serverless): record member roles in kind:39002 (creator=owner)
The serverless membership event only stored bare ['p', pubkey] tags with
no role, so every member — including the channel creator — read back as
'member'. This hid the member-invite card on private channels (it requires
owner/admin) and diverged from server mode, where the relay assigns the
creator 'owner'.

Store roles as the 4th p-tag element (['p', pubkey, '', role]) — the NIP-29
convention channel_members_from_event already reads. Creator = owner; new
members default to 'member'; role changes remove+re-add; existing members
keep their role across read-modify-write. Matches server-mode data shape.
2026-06-02 14:23:57 +10:00
Michael Neale 6f5f6df71c fix(serverless): build ChannelInfo locally on create/open_dm (fixes 'nothing happened')
The serverless create_channel and open_dm re-queried the relay for the
kind:39000 they had just published, to build ChannelInfo. On a public relay
that immediate round-trip races against propagation/indexing lag and can
return empty, throwing 'metadata not yet available' — surfacing as a private
channel / DM that silently fails to appear in the UI.

Both now derive ChannelInfo directly from the locally-signed metadata event
(we created it, so is_member = true), with no relay round-trip and no race.
2026-06-02 13:37:12 +10:00
Michael Neale a568920cf9 test(serverless): live agent encrypted-receive test + RELAY_URL override
Add a live, #[ignore]d end-to-end test proving a HarnessRelay in serverless
mode (as a managed agent runs) receives + decrypts a NIP-17 gift-wrapped
message over a real public relay and surfaces it via next_event() with the
verified sender preserved. Matches the repo's e2e convention (RELAY_URL env,
default damus). Also wire RELAY_URL into the existing serverless live tests so
they aren't hardcoded to damus.
2026-06-02 12:47:00 +10:00
Michael Neale ba335aed2a refactor(serverless): remove dead code introduced on branch
- Delete orphaned HarnessRelay::connect() wrapper (callers use
  connect_with_mode); fold its doc into connect_with_mode.
- Gate BgState::new() behind #[cfg(test)] (test-only now that the
  runtime path uses with_serverless) instead of #[allow(dead_code)].
- Move KIND_GIFT_WRAP into the encrypted test module (test-only).

No #[allow(dead_code)] / cfg_attr dead-code suppressions remain on
branch-new code. Verified with RUSTFLAGS='-D dead_code -D unused'.
2026-06-02 11:32:39 +10:00
Michael Neale 191de54143 docs(serverless): document multi-relay, agents, and NIP-17 encryption 2026-06-02 10:33:00 +10:00
Michael Neale b93eb61570 feat(serverless): agents receive NIP-17 encrypted messages (sprout-acp)
The agent harness now subscribes to its own gift-wrap inbox (kind 1059,
#p = agent) alongside membership notifications, unwraps each, recovers
the inner kind-9 rumor (verified sender + h tag), and forwards it as a
normal SproutEvent. The respond-to allowlist gate and rule matching run
unchanged — so an agent in an encrypted private channel only responds to
blessed npubs, exactly as on the Sprout server.

- send_gift_wrap_subscribe (mirrors observer-control), tied to membership
  sub (active + reconnect restore)
- dispatch: 1059 on GIFT_WRAP_SUB_ID -> UnwrappedGift -> rumor_to_event
- rumor_to_event: UnsignedEvent -> Event (seal already verified sender;
  pipeline never reads .sig)
- nip59 enabled workspace-wide; unit test covers unwrap->channel event
2026-06-02 10:15:56 +10:00
Michael Neale 74deb9cf77 feat(serverless): NIP-17 encrypted DMs + private channels (desktop)
Serverless private channels and DMs are made private by encryption (no
server to enforce access). Messages are NIP-17 gift-wrapped (kind 1059)
to every member; the relay only sees opaque blobs addressed by #p.

- crate::encrypted: build_gift_wraps (one per member) + unwrap_gift
- send_channel_message: routes serverless DM/private channels through
  encryption (rumor=kind9 with h tag -> seal -> wrap per member)
- decrypt_gift_wrap command + relayClient encrypted fetch/subscribe
  (query kind1059 #p=me, decrypt, route by inner h tag)
- proven end-to-end over wss://relay.damus.io (group A->{A,B,C}, B reads)

Agent-side gift-wrap support (sprout-acp) follows separately.
2026-06-02 10:10:10 +10:00
Michael Neale 8934a07051 feat(serverless): multi-relay support — publish-to-all, read-deduped
Serverless workspaces now take a comma-separated relay list (seeded with the
5 default public relays) for redundancy:
- ws_relay query/submit/publish fan out to all relays concurrently; reads
  merge+dedup by event id, writes succeed if any relay accepts
- relay_ws_urls_with_override parses the comma list; live WS uses first relay
- setup UIs (Welcome + AddWorkspace) have multi-select relay chips
- integration test proves publish-to-2 / read-deduped-from-2
2026-06-02 09:25:24 +10:00
Michael Neale 858cb3b02a fix(serverless): allow self-tagging on channel membership events
The 'join to participate, nothing happens' bug: when a user creates or
joins a serverless channel they p-tag themselves (the signer). nostr 0.44
strips self-referencing p tags at sign time unless .allow_self_tagging()
is set, so kind:39002 membership events were published with an EMPTY
member list. get_channels then queries 39002 by #p:[me], finds nothing,
and the UI stays stuck on 'join to participate' forever.

(This worked in the old TS/slackest codebase because nostr-tools does not
perform that scrub.)

Fix: add .allow_self_tagging() to the serverless 39000 + 39002 builders
in both desktop events.rs and the shared sprout-sdk builders (CLI/agents).

Reproduced and verified end-to-end against wss://relay.damus.io via a new
#[ignore] integration test (serverless_create_join_roundtrip): create ->
join -> both members visible. Added no-network unit regression guards
(serverless_{members,metadata}_keeps_self_p_tag) in both crates.

clippy clean (desktop+sdk+cli); desktop + sdk tests pass; file-size gate ok.
2026-06-02 08:43:09 +10:00
Michael Neale fc832f4d5d fix(serverless): membership via direct kind:39002 (join/leave/add/remove) + CLI channel ops
In serverless mode there's no relay to process membership command kinds
(9021 join, 9022 leave, 9000/9001 add/remove member). The client now mutates
the replaceable kind:39002 member-list event directly (read-modify-write).

- desktop: join/leave/add/remove_channel_member publish 39002 directly
- sprout-sdk: build_channel_metadata_serverless (39000) +
  build_channel_members_serverless (39002), shared builders
- sprout-cli: channels create/join/leave/add-member/remove-member serverless
  branches (fixes agent replies + CLI parity with desktop)

Verified end-to-end against wss://relay.damus.io: create -> list -> send ->
read all round-trip with zero Sprout infrastructure.
2026-06-02 08:33:06 +10:00
Michael Neale c508603543 feat(serverless): default public relays from deez mesh client
Add DEFAULT_PUBLIC_RELAYS (sourced from deez/crates/mesh-client
DEFAULT_RELAYS) as quick-pick chips when creating a serverless
workspace, in both the Add Workspace dialog and the Welcome screen.

- relay.damus.io, nos.lol, relay.nostr.band, nostr.land, nostr.wine
- chips one-tap fill the relay URL field (serverless mode only)
- DEFAULT_SERVERLESS_RELAY prefills when serverless is first enabled
2026-06-02 08:05:57 +10:00
Michael Neale 84f84931f1 feat(serverless): run channels, DMs, messages, and agents against a generic Nostr relay
Adds a 'serverless' workspace mode that points the desktop app at any generic
public Nostr relay (e.g. wss://relay.damus.io) with zero Sprout server
infrastructure — no Postgres, no HTTP /query|/events bridge, no NIP-98 auth.
Reuses Sprout's native event kinds (39000 channel metadata, 39002 membership,
kind 9 messages, h-tag scoping); 'serverless' is purely a transport + auth
concern, so the server-mode path is untouched.

Transport: reads/writes go over plain WebSocket (REQ/EOSE, EVENT/OK) instead
of the HTTP bridge; NIP-42 AUTH is answered only if the relay challenges.

Desktop backend:
- AppState.serverless flag + is_serverless(); apply_workspace gains a flag
- ws_relay.rs: query_relay_ws / submit_event_ws / publish_signed_event_ws
- relay.rs: query_relay/submit_event + agent profile sync branch to WS
- events.rs: build_channel_metadata_serverless (39000) + members (39002)
- create_channel/open_dm publish those events directly (DM ids = UUIDv5 over
  sorted participants so both sides converge without a server)
- managed agents inherit SPROUT_SERVERLESS into the ACP subprocess env

Agents (full support, not a follow-up):
- sprout-acp: RestClient + HarnessRelay gain serverless mode; query/submit use
  plain WS, NIP-42 handshake skipped on (re)connect; SPROUT_SERVERLESS env/arg
- sprout-cli: SproutClient serverless WS query/submit; --serverless flag,
  to_ws_url helper, NetworkMsg error variant
- npub respond-to permissions are in-process and work unchanged

Frontend:
- Workspace.mode + workspaceMode()/isServerlessWorkspace() helpers
- relayClient.setServerless() skips the AUTH-wait on connect; late challenges
  still answered so writes succeed on relays that require auth
- ServerlessContext/useIsServerless() hides search, pulse, projects, workflows
- Serverless toggle in the Add Workspace dialog and Welcome screen

Docs: docs/SPROUT_LITE_MODE.md updated with the implementation + testing steps.
2026-06-01 20:42:29 +10:00
bc23620fcf Fix desktop glass chrome and inbox previews (#793)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-30 20:27:16 -07:00
Will PflegerandGitHub 9b9cf46127 refactor(just): slim down mobile-dev to just run Flutter (#801) 2026-05-29 20:44:32 -04:00
Will PflegerandGitHub 2a03851527 refactor: consolidate infra management into justfile + add mobile-dev (#797) 2026-05-29 18:26:24 -04:00
WesandGitHub 995189c223 chore(release): release version 0.3.5 (#799) v0.3.5 2026-05-29 14:48:49 -07:00
b820420909 feat(mobile): Pulse polish — flat feed, compose page, shared filter chips, like + accent fixes (#796)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
2026-05-29 14:33:45 -07:00
Will PflegerandGitHub 10f37e4cab feat(desktop): add standalone Playwright screenshot helper (#795) 2026-05-29 17:10:56 -04:00
Will PflegerandGitHub f34a21d32f feat(sprout-agent): load AGENTS.md and SKILL.md into system prompt (#762) 2026-05-29 14:44:33 -04:00
Will PflegerandGitHub 85861f33fe feat: add code block support to message composer (#788) 2026-05-29 11:38:10 -07:00
Will PflegerandGitHub 7beb0f8e68 fix(desktop): reap orphaned agent processes on shutdown and restart (#787) 2026-05-29 18:18:55 +00:00
WesandGitHub 8fc27b6554 chore(release): release version 0.3.4 (#786) v0.3.4 2026-05-29 09:50:08 -07:00
d77b111b15 Update desktop navigation chrome and search (#779)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 08:26:41 -07:00
tlongwell-blockandGitHub 5ee2cd0517 feat(desktop): reload webview on Cmd/Ctrl+R (#785) 2026-05-29 10:24:56 -04:00
Will PflegerandGitHub fa7febe40f fix(desktop): sync persona pack directory across worktree instances (#782) 2026-05-28 20:41:23 -04:00
WesandGitHub a28a0e6b42 chore(release): release version 0.3.3 (#781) v0.3.3 2026-05-28 16:51:47 -07:00
WesandGitHub c761a76ff2 fix(release): sync release tags during preflight (#780) 2026-05-28 16:29:25 -07:00
Will PflegerandGitHub 3f3ec64791 feat(desktop): thread-aware notifications with mutable follow/mute controls (#761) 2026-05-28 18:43:41 -04:00
Will PflegerandGitHub 03e678cfbc fix(desktop): improve model picker message and runtime dropdown clarity (#778) 2026-05-28 18:21:12 -04:00
WesandGitHub 9db8f6ccb8 desktop: float unread indicator + fix sidebar scroll jump (#777)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
2026-05-28 15:15:19 -07:00
Will PflegerandGitHub 3fbee555f0 chore(hooks): standardize check/fix convention with auto-fix pre-commit (#776) 2026-05-28 17:42:35 -04:00
0f89ad1692 web: clickable repo tree + per-file blob viewer (#773)
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
2026-05-28 15:55:03 -04:00
tlongwell-blockandGitHub 61297ac80a fix(agent): keep parallel tool-result messages contiguous on OpenAI Chat (Databricks image fix) (#770)
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
2026-05-28 15:25:51 -04:00
Will PflegerandGitHub 5f2423c231 fix(release): fetch tags so changelog tracks versions correctly (#775) 2026-05-28 19:17:35 +00:00
WesandGitHub 8da9acbad9 chore(release): release version 0.3.2 (#774) v0.3.2 2026-05-28 12:00:40 -07:00
WesandGitHub 1218572fa0 feat(mobile): add Pulse social feed tab (#772)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
2026-05-28 11:09:44 -07:00
WesandGitHub fec6a683ea feat(sidebar): add More unread floating buttons (#771)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
2026-05-28 11:05:02 -07:00
Taylor HoandGitHub 8eedec7403 chore: improve markdown spacing (#766) 2026-05-28 17:35:10 +00:00
Taylor HoandGitHub 835a44aad0 fix: prevent inline links rendering in 2-column grid layout (#767) 2026-05-28 17:34:35 +00:00
WesandGitHub 9a10e6281e chore(release): release version 0.3.1 (#769) v0.3.1 2026-05-28 09:10:28 -07:00
WesandGitHub 4222a758c6 [codex] Default release command to patch bump (#768) 2026-05-28 08:48:12 -07:00
WesandGitHub 30654e95f6 Polish desktop Pulse and Home views (#764)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
2026-05-28 08:17:02 -07:00