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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,26 @@ teaches something new in the tour, set the newest entry's `version` in

## [Unreleased]

## [0.12.0] - 2026-10-13

### Changed

- **Works with a stock blyg.** Burrow now runs fully against an unmodified Blygger Studio (0.40 and later, checked against 0.46.1), with no server patches. Reading syncs from the studio's own imported feed (`GET /api/reading/imported`, paged by cursor), read marks sync through its read-state routes, and lineage, citations, the full author and disclosed generation sources come with each post. Servers running the older owner-API extensions keep working. [Server requirements](https://aneeshsathe.github.io/blygger-desktop/server.html) and `docs/SERVER.md` describe what's needed.
- **Sign in with a studio token or the browser.** The server owner token from the old extensions is gone from sign-in. Use Sign in with browser, or a token from Studio → More → Client access (allow `reading:state` so read marks sync). If your blyg dropped the old owner token, Burrow opens the sign-in sheet after updating; sign in once and everything queued is sent.
- **Preview matches the studio 0.46.1 renderer.** Embeds are no longer baked into the HTML (the poster still shows), and `[TK]`, `[=]` and `[/TK]` inside code anywhere in a scope are plain text (studio 0.41.1).
- **"Drew on this".** A mention whose relation is `source` (a post that used yours as a generation source) shows as "drew on this".

### Added

- **Publish warns about quotes in generated text.** If a TK block's output holds an own-line `![[id]]` that its instruction didn't name, the publish sheet says so before you publish: publishing turns it into a quote and notifies that blyg. Preview now shows it as the quote publish will make (decision #60).
- **Generated text is marked as the publisher's own claim.** In the Reader, text a blyg marks as generated gets a tooltip, and the post ends with a short note that the disclosure is the publisher's own and isn't verified.

### Fixed

- **Reading sync against studio 0.40 and later.** Burrow asked for 500 rows a page in the old shape, which a stock studio refuses, so reading never synced there.
- **A refused sign-in stops syncing cleanly.** When the blyg rejects Burrow's token (for example after a server change), Burrow stops sending and pulling, keeps every queued change, and opens the sign-in sheet with an explanation, instead of retrying.
- **Authors and citations come through whole.** The full author object is kept, and `cited` survives on replies (`stub_of`) and remote references.

## [0.11.0] - 2026-10-09

### Added
Expand Down
18 changes: 9 additions & 9 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ members = ["crates/blyg-core", "crates/blyg-ai", "crates/blyg-render", "crates/b

[workspace.package]
edition = "2024"
version = "0.11.0"
version = "0.12.0"
license = "MIT"
description = "A fast, local-first macOS studio for Blygger blogs"
# Placeholder repo; set it with scripts/set-repo.sh (see packaging/config.env).
Expand Down
3 changes: 3 additions & 0 deletions crates/blyg-ai/tests/prompts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ fn continue_outline_and_reply_are_tk_scopes() {
content_html: String::new(),
author: Some(Author {
name: Some("Sam".into()),
extra: Default::default(),
url: None,
}),
page: Some("https://them.example/f/R1/".into()),
Expand All @@ -235,6 +236,8 @@ fn continue_outline_and_reply_are_tk_scopes() {
transclusions: vec![],
pinned_version_retained: None,
lineage_known: false,
source_url: None,
generated: None,
};
let p = Scripted::new(&["Agreed, and…"]);
let out = reply_draft(&p, &item, None, &CancelFlag::new(), &mut |_| {}).unwrap();
Expand Down
10 changes: 5 additions & 5 deletions crates/blyg-app/src/about/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,18 +158,18 @@ impl ConnectionInfo {
}
}

/// Connected to a server without Burrow's extensions (a stock
/// blygger-studio): the About window says what that limits.
/// Connected to a server without `GET /api/reading/imported` (a studio
/// older than 0.40): the About window says what that limits.
pub fn limited(&self) -> bool {
matches!(self, ConnectionInfo::Live { caps, .. }
if caps.iter().any(|c| c.label == EXTENSIONS && !c.on))
}
}

const EXTENSIONS: &str = "Server extensions";
const EXTENSIONS: &str = "Full reading feed";

/// The capabilities, in display order, from what the backend recorded:
/// the docs/SERVER.md extensions at all, read state across Macs, and AI
/// the full reading feed (studio 0.40+, or the old fork), read state across Macs, and AI
/// disclosure for text generated in the app.
pub fn capabilities(extensions: bool, read_sync: bool, provenance: bool) -> Vec<Capability> {
vec![
Expand Down Expand Up @@ -363,7 +363,7 @@ Auto-update: notify
Last check: 2026-09-21 15:13 UTC
Update: up to date
Blyg: blyg.example.com
Server extensions: yes
Full reading feed: yes
Read-state sync: no
AI disclosure: yes
";
Expand Down
4 changes: 2 additions & 2 deletions crates/blyg-app/src/about/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,15 +496,15 @@ impl Render for AboutView {
.child(section("CONNECTION"))
.child(row("Blyg", text(r.connection.summary())))
.children(caps)
// A stock server: what that limits (as the sheet said).
// A studio older than 0.40: what that limits (as the sheet said).
.when(r.connection.limited(), |d| {
d.child(
div()
.pl(px(114.))
.pt(px(4.))
.text_color(p.muted)
.line_height(relative(1.4))
.child("Stock blygger-studio. Limited here:")
.child("Older blygger-studio. Limited here:")
.children(
crate::app::server_notice::LIMITS
.iter()
Expand Down
20 changes: 20 additions & 0 deletions crates/blyg-app/src/ai/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,23 @@ pub fn resolve_source(
})
}

/// A source's blyg when it is someone else's post (an imported one, not
/// this blyg's own): upstream records remote sources with their origin
/// (studio 0.41+). The same lookup order as `resolve_source`.
fn remote_origin(items: &[Item], reading: &[ReadingItem], id: &str) -> Option<String> {
if items
.iter()
.any(|i| i.server_id.as_ref().is_some_and(|s| s.0 == id))
{
return None;
}
reading
.iter()
.find(|r| r.remote_id == id)
.map(|r| r.origin.clone())
.filter(|o| !o.is_empty())
}

/// A fill/regenerate request, built on the UI thread and run elsewhere.
#[derive(Debug, Clone)]
pub struct FillJob {
Expand Down Expand Up @@ -107,6 +124,7 @@ pub fn prepare_fill(
.map(|id| ProvenanceSource {
id: id.clone(),
version: versions.iter().find(|(v, _)| v == id).and_then(|(_, v)| *v),
origin: remote_origin(items, reading, id),
})
.collect();
Ok(FillJob {
Expand Down Expand Up @@ -301,10 +319,12 @@ mod tests {
ProvenanceSource {
id: ID.into(),
version: Some(2),
origin: None,
},
ProvenanceSource {
id: "zzzzzzzzzzzzzzzzzzzzzzzzzz".into(),
version: None,
origin: None,
},
],
..prov("m2")
Expand Down
1 change: 1 addition & 0 deletions crates/blyg-app/src/ai/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ impl MainView {
origin: item.origin.clone(),
id: item.remote_id.clone(),
version: item.version,
cited: None,
};
let body = super::reading::vm::stub_body(&item.remote_id);
let stub = match self.backend.create_stub(&of, &body) {
Expand Down
47 changes: 34 additions & 13 deletions crates/blyg-app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -487,12 +487,7 @@ impl MainView {
// An ended browser sign-in: ask for a new one (the
// sheet says why) rather than retrying.
(Err(CoreError::Unauthorized), _) if v.sheet.is_none() => {
v.show_toast(
"Your blyg signed Burrow out",
Some("Sign in again to sync".into()),
cx,
);
v.open_connect(window, cx);
v.signed_out(window, cx);
}
(Err(e), _) => {
v.show_toast(format!("Couldn't load from the blyg: {e}"), None, cx)
Expand Down Expand Up @@ -766,10 +761,9 @@ impl MainView {
};
match (secret.is_empty(), by_password) {
(true, true) => Err("Type your blyg's studio password".to_string()),
(true, false) => Err(
"Paste an API token: make one in Studio → More → Client access, or use your server's owner token"
.to_string(),
),
(true, false) => {
Err("Paste an API token: make one in Studio → More → Client access".to_string())
}
(false, true) => Ok((u, Credential::Password(secret))),
(false, false) => Ok((u, Credential::Token(secret))),
}
Expand Down Expand Up @@ -849,7 +843,7 @@ impl MainView {
Ok(_) => {
// A studio older than 0.28: the password works.
v.set_connect_way(ConnectWay::Password, window, cx);
"This blyg has no browser sign-in (its studio is older than 0.28). Use its studio password, or an owner token.".to_string()
"This blyg has no browser sign-in (its studio is older than 0.28). Use its studio password.".to_string()
}
Err(CoreError::Offline) => {
"Couldn't reach that address. Check the URL and your connection."
Expand Down Expand Up @@ -1309,6 +1303,34 @@ impl MainView {
CoreEvent::Error(msg) => self.show_toast(msg, None, cx),
CoreEvent::ServerOutdated => self.server_outdated(cx),
CoreEvent::ServerLimited => self.server_limited(window, cx),
CoreEvent::SignedOut => {
if self.sheet.is_none() {
self.signed_out(window, cx);
}
}
}
}

/// The blyg refused the stored credential (401): a revoked or expired
/// token, an ended browser sign-in, or an owner token a blyg no longer
/// takes. Sync has stopped (no retries) and changes wait in the outbox;
/// ask for a new sign-in, browser first.
fn signed_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.show_toast(
"Your blyg signed Burrow out",
Some("Sign in again to sync; your changes wait on this Mac".into()),
cx,
);
self.open_connect(window, cx);
if let Some(Sheet::Connect { error, .. }) = self.sheet.as_mut()
&& error.is_none()
{
*error = Some(
"Your blyg no longer accepts Burrow's saved sign-in. Sign in with the \
browser, or paste a new API token from Studio → Access, or use the \
Studio password. Changes made meanwhile are kept and sync after."
.into(),
);
}
}

Expand Down Expand Up @@ -3098,8 +3120,7 @@ impl MainView {
"Make a token in your blyg's Studio → More → Client access: choose REST \
API and every permission (read, draft, publish, manage, and reading:state \
where offered). It lasts \
30 days. A server with Burrow's extensions can use its owner token \
here instead. Either is kept in your macOS Keychain."
30 days. It is kept in your macOS Keychain."
}
ConnectWay::Password => {
"The password you sign in to the studio with. It works with any \
Expand Down
4 changes: 4 additions & 0 deletions crates/blyg-app/src/composer/mention.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,7 @@ mod tests {
content_html: String::new(),
author: Some(Author {
name: Some(author.into()),
extra: Default::default(),
url: None,
}),
page: None,
Expand All @@ -560,6 +561,8 @@ mod tests {
forked_from: None,
transclusions: vec![],
lineage_known: false,
source_url: None,
generated: None,
}
}

Expand Down Expand Up @@ -705,6 +708,7 @@ mod tests {
origin: ADA.into(),
id: "a1".into(),
version: 1,
cited: None,
});
let linked = own("As [Rue](https://rue.blyg.example.org/f/x) said.");
let items = vec![reply, linked];
Expand Down
15 changes: 15 additions & 0 deletions crates/blyg-app/src/fake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ fn sample_reading() -> ReadingItem {
content_html: "<p>A tide pool is a small ocean that forgets, twice a day, that it belongs to a larger one.</p>\n".into(),
author: Some(Author {
name: Some("Shoreline Notes".into()),
extra: Default::default(),
url: None,
}),
page: None,
Expand All @@ -60,9 +61,23 @@ fn sample_reading() -> ReadingItem {
forked_from: None,
transclusions: vec![],
lineage_known: false,
source_url: None,
generated: None,
}
}

/// The tour's thread whose generated text echoed a quote its instruction
/// never asked for (decision #60): the publish sheet warns about it, and
/// the preview shows the quote publishing would make.
pub fn echoed_quote_sample() -> String {
format!(
"Rock pools at dusk\n\n\
The light leaves the shelf before the water does.\n\n\
[TK]a closing line about the tide coming back[=]The tide comes back for everything it lent.\n\n\
![[{SAMPLE_QUOTE_ID}]][/TK]\n"
)
}

/// The full editor's sample thread (`BLYGGER_DEMO=studio`): a quote, a
/// generated TK span, a YouTube link and a quote that can't be resolved.
pub fn studio_sample() -> String {
Expand Down
Loading
Loading