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
4 changes: 2 additions & 2 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 crates/synapse/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "synapse"
# Bumping this on main is the release trigger. Keep it in step with
# crates/synapsecore, which is where `synapse version` reads its number from.
version = "1.12.0"
version = "1.13.0"
edition.workspace = true
description = "Local memory and scoped credentials for developer tools"

Expand Down
79 changes: 58 additions & 21 deletions crates/synapse/src/ui/dashboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ pub struct Dashboard {
usagedays: Option<u32>,
surfaces: Vec<synapsecore::usage::Surface>,
statserror: Option<String>,
statsloading: bool,
statsrun: u64,
mesherror: Option<String>,
skillrows: Vec<skills::Row>,
skillunmanaged: Vec<(String, String)>,
Expand Down Expand Up @@ -378,6 +380,8 @@ impl Dashboard {
usagedays: Some(30),
surfaces: Vec::new(),
statserror: None,
statsloading: false,
statsrun: 0,
mesherror: meshdata.error,
skillrows: skilldata.rows,
skillunmanaged: skilldata.unmanaged,
Expand Down Expand Up @@ -468,37 +472,67 @@ impl Dashboard {

fn showstats(&mut self, cx: &mut Context<Self>) {
self.page = Page::Stats;
self.refreshstats(cx);
self.refreshstats(true, cx);
}

/// The startup recall is measured outside any project, since the app has
/// none open.
fn refreshstats(&mut self, cx: &mut Context<Self>) {
fn refreshstats(&mut self, measure_surfaces: bool, cx: &mut Context<Self>) {
let Some(brain) = self.brain.clone() else {
self.statserror = Some("Synapse could not open its store.".to_owned());
self.statsloading = false;
cx.notify();
return;
};
let days = self.usagedays;
let result = block(async move {
let report = synapsecore::usage::report(&brain, days).await?;
let surfaces = synapsecore::usage::surfaces(&brain, &files::soul()?, None).await?;
anyhow::Ok((report, surfaces))
});
match result {
Ok((report, surfaces)) => {
self.usage = Some(report);
self.surfaces = surfaces;
self.statserror = None;
}
Err(error) => self.statserror = Some(format!("Could not read stats: {error}")),
}
self.statsloading = true;
self.statserror = None;
cx.notify();
self.statsrun = self.statsrun.wrapping_add(1);
let run = self.statsrun;
let days = self.usagedays;
let measure_surfaces = measure_surfaces || self.surfaces.is_empty();
cx.spawn(async move |this, cx| {
let result = cx
.background_executor()
.spawn(async move {
let report = block(synapsecore::usage::report(&brain, days));
let surfaces = measure_surfaces.then(|| {
files::soul().and_then(|soul| {
block(synapsecore::usage::surfaces(&brain, &soul, None))
})
});
(report, surfaces)
})
.await;
this.update(cx, |this, cx| {
if this.statsrun != run {
return;
}
this.statsloading = false;
let (report, surfaces) = result;
let mut errors = Vec::new();
match report {
Ok(report) => this.usage = Some(report),
Err(error) => errors.push(format!("usage: {error}")),
}
if let Some(surfaces) = surfaces {
match surfaces {
Ok(surfaces) => this.surfaces = surfaces,
Err(error) => errors.push(format!("startup cost: {error}")),
}
}
this.statserror = (!errors.is_empty())
.then(|| format!("Could not read stats: {}", errors.join("; ")));
cx.notify();
})
.ok();
})
.detach();
}

fn setstatswindow(&mut self, days: Option<u32>, cx: &mut Context<Self>) {
self.usagedays = days;
self.refreshstats(cx);
self.refreshstats(false, cx);
}

fn setprice(&mut self, dollars: f64, cx: &mut Context<Self>) {
Expand All @@ -513,7 +547,7 @@ impl Dashboard {
)),
Err(error) => Notice::Error(format!("Could not change the price: {error}")),
};
self.refreshstats(cx);
self.refreshstats(false, cx);
}

fn showmap(&mut self, cx: &mut Context<Self>) {
Expand Down Expand Up @@ -1549,7 +1583,7 @@ impl Dashboard {
self.brain = Some(brain);
}
match self.page {
Page::Stats => self.refreshstats(cx),
Page::Stats => self.refreshstats(true, cx),
Page::Skills => self.refreshskills(cx),
Page::Servers => self.refreshservers(cx),
Page::Memories => self.refreshimports(cx),
Expand Down Expand Up @@ -2881,11 +2915,14 @@ impl Dashboard {
surfaces: self.surfaces.clone(),
days: self.usagedays,
error: self.statserror.clone(),
loading: self.statsloading,
},
stats::Actions {
window: Box::new(window),
price: Box::new(price),
refresh: Box::new(cx.listener(|this, _, _, cx| this.refreshstats(cx))),
refresh: Box::new(
cx.listener(|this, _, _, cx| this.refreshstats(true, cx)),
),
},
cx,
))
Expand Down Expand Up @@ -3481,7 +3518,7 @@ fn connectionserver() -> Option<PathBuf> {
fn initialpage() -> Page {
match std::env::var("SYNAPSE_PAGE").as_deref() {
Ok("map") => Page::Map,
Ok("stats") => Page::Stats,
Ok("stats" | "savings") => Page::Stats,
Ok("memory") => Page::Memories,
Ok("mesh") => Page::Mesh,
Ok("skills") => Page::Skills,
Expand Down
13 changes: 9 additions & 4 deletions crates/synapse/src/ui/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub struct View {
pub surfaces: Vec<Surface>,
pub days: Option<u32>,
pub error: Option<String>,
pub loading: bool,
}

pub struct Actions {
Expand Down Expand Up @@ -72,6 +73,10 @@ pub fn render(view: View, actions: Actions, cx: &App) -> AnyElement {
);
}

if view.loading && view.report.is_none() {
page = page.child(Text::new("Loading stats…").size(Size::Sm).dimmed());
}

let warning = theme.warning().hsla();
let carried =
(!view.surfaces.is_empty()).then(|| persession(&view.surfaces, border, surface, warning));
Expand Down Expand Up @@ -352,10 +357,10 @@ fn persession(surfaces: &[Surface], border: Hsla, background: Hsla, warning: Hsl
.flex()
.flex_col()
.gap(px(2.0))
.child(Text::new("In every session").size(Size::Sm).bold())
.child(Text::new("Startup footprint").size(Size::Sm).bold())
.child(
Text::new(
"Loaded before the first turn by each connected tool. Same numbers as `synapse tokens`.",
"Potential startup cost. Skill descriptions count the library; each tool loads only its installed skills.",
)
.size(Size::Xs)
.dimmed(),
Expand All @@ -370,9 +375,9 @@ fn persession(surfaces: &[Surface], border: Hsla, background: Hsla, warning: Hsl
}))
.child(div().h(px(1.0)).bg(border))
.child(line(
"Total",
"Total potential",
total as i64,
"Per session, per connected tool".to_owned(),
"If all library skills are installed in the tool".to_owned(),
));
for text in surfaces.iter().filter_map(|item| item.warning.clone()) {
panel = panel.child(
Expand Down
2 changes: 1 addition & 1 deletion crates/synapsecore/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 crates/synapsecore/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ name = "synapsecore"
# Keep this in step with crates/synapse: `synapse version` reads the version of
# the crate that holds the command, and the two ship as one product. Only the
# desktop crate's version triggers the release workflow.
version = "1.12.0"
version = "1.13.0"
edition = "2024"
description = "Memory, credentials, and the command surface for Synapse, without a window"

Expand Down
11 changes: 7 additions & 4 deletions crates/synapsecore/src/cli/tokens.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! `synapse tokens`: what Synapse costs a session before the first turn.
//!
//! Recall has a budget. The guidance, the tool definitions, and every skill
//! description don't, and they're paid once per session per connected tool
//! description don't, and installed descriptions are paid once per session per connected tool
//! whether or not a memory is ever recalled. This counts them so the settings
//! that move them (optimization, mesh, learn) have a number beside them. It
//! changes nothing. The Stats page and `doctor` show the same measurement.
Expand Down Expand Up @@ -37,7 +37,7 @@ pub fn tokens(arguments: &[OsString]) -> Result<Outcome> {
return Ok(Outcome::Exit(0));
}

println!("What Synapse costs a session, before the first turn");
println!("Synapse's potential session cost, before the first turn");
println!();
for line in &lines {
println!(
Expand All @@ -50,12 +50,15 @@ pub fn tokens(arguments: &[OsString]) -> Result<Outcome> {
}
println!(" {}", "─".repeat(62));
println!(
" {:<16} {:>7} chars ~{:>6} tokens per session, per connected tool",
"Total",
" {:<16} {:>7} chars ~{:>6} tokens if all library skills are installed",
"Total potential",
thousands(total),
thousands(total / PERTOKEN)
);
println!();
println!(
" Skill descriptions count the whole library; each tool loads only its installed skills."
);
println!(" Token figures are characters ÷ {PERTOKEN}, not a tokenizer. Counts are exact.");

let warnings: Vec<&Surface> = lines.iter().filter(|line| line.warning.is_some()).collect();
Expand Down
10 changes: 5 additions & 5 deletions crates/synapsecore/src/usage/surface.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! What every session carries before the first turn: Synapse's guidance, its
//! tool definitions, the skill descriptions, and the session hook's recall.
//! A session's startup footprint: Synapse's guidance, its tool definitions,
//! the potential skill description cost, and the session hook's recall.
//! Nothing here has a budget except the recall, which is why it's counted.
//! Character counts are exact; tokens are an estimate.

Expand Down Expand Up @@ -108,8 +108,8 @@ fn tooldefinitions(mesh: bool, learn: bool) -> Surface {
}
}

/// Names and descriptions, which a tool loads for every installed skill. Bodies
/// are read on demand and don't count.
/// Names and descriptions in the library. This is potential cost: a tool only
/// loads the skills installed in it. Bodies are read on demand and don't count.
fn skills() -> Result<Surface> {
let (skills, _) = crate::skill::library::all()?;
let mut chars = 0;
Expand All @@ -126,7 +126,7 @@ fn skills() -> Result<Surface> {
Ok(Surface {
label: "Skill library",
detail: format!(
"{}, descriptions only",
"{} in library, if installed",
plural(skills.len(), "skill", "skills")
),
chars,
Expand Down
2 changes: 1 addition & 1 deletion site/docs/config/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ <h2 id="environment">Environment variables</h2>
<tr><td><code>SYNAPSE_HOME</code></td><td>Replaces the home directory used for tool files and the default CLI path.</td><td>Integration testing without touching real user files.</td></tr>
<tr><td><code>SYNAPSE_BIN</code></td><td>Replaces the full CLI installation destination.</td><td>Install outside <code>~/.local/bin</code>.</td></tr>
<tr><td><code>SYNAPSE_PROJECT_DIR</code></td><td>Provides the fallback project folder for MCP calls that do not carry one, and is what a launched agent is told to treat as home.</td><td>A client that launches the server outside the project working directory. Set automatically by <code>synapse launch</code> and <code>relay launch</code>.</td></tr>
<tr><td><code>SYNAPSE_PAGE</code></td><td>Chooses which dashboard page opens first: <code>map</code>, <code>stats</code>, <code>memory</code>, <code>mesh</code>, <code>skills</code>, <code>vaults</code>, or <code>settings</code>. Anything else opens Connections.</td><td>Opening the app straight to the screen you want.</td></tr>
<tr><td><code>SYNAPSE_PAGE</code></td><td>Chooses which dashboard page opens first: <code>map</code>, <code>stats</code> (or the earlier <code>savings</code> value), <code>memory</code>, <code>mesh</code>, <code>skills</code>, <code>vaults</code>, or <code>settings</code>. Anything else opens Connections.</td><td>Opening the app straight to the screen you want.</td></tr>
<tr><td><code>CODEX_HOME</code></td><td>Replaces the Codex home directory used for detection and for reading an existing Codex memory store.</td><td>A non-default Codex installation, or testing an import without touching real files.</td></tr>
<tr><td><code>PI_CODING_AGENT_DIR</code></td><td>pi's own variable for its agent directory, which Synapse reads for the same reason: detection, the instruction pointer, and the skills folder.</td><td>A non-default pi installation.</td></tr>
<tr><td><code>SYNAPSE_PI_PACKAGE</code></td><td>Replaces the package a connection to pi installs, and the source detection compares against. Defaults to <code>npm:synapse-pi</code>.</td><td>Working on the package from a checkout instead of npm.</td></tr>
Expand Down
2 changes: 1 addition & 1 deletion site/search.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion website/docs/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const config: Page = {
<tr><td><code>SYNAPSE_HOME</code></td><td>Replaces the home directory used for tool files and the default CLI path.</td><td>Integration testing without touching real user files.</td></tr>
<tr><td><code>SYNAPSE_BIN</code></td><td>Replaces the full CLI installation destination.</td><td>Install outside <code>~/.local/bin</code>.</td></tr>
<tr><td><code>SYNAPSE_PROJECT_DIR</code></td><td>Provides the fallback project folder for MCP calls that do not carry one, and is what a launched agent is told to treat as home.</td><td>A client that launches the server outside the project working directory. Set automatically by <code>synapse launch</code> and <code>relay launch</code>.</td></tr>
<tr><td><code>SYNAPSE_PAGE</code></td><td>Chooses which dashboard page opens first: <code>map</code>, <code>stats</code>, <code>memory</code>, <code>mesh</code>, <code>skills</code>, <code>vaults</code>, or <code>settings</code>. Anything else opens Connections.</td><td>Opening the app straight to the screen you want.</td></tr>
<tr><td><code>SYNAPSE_PAGE</code></td><td>Chooses which dashboard page opens first: <code>map</code>, <code>stats</code> (or the earlier <code>savings</code> value), <code>memory</code>, <code>mesh</code>, <code>skills</code>, <code>vaults</code>, or <code>settings</code>. Anything else opens Connections.</td><td>Opening the app straight to the screen you want.</td></tr>
<tr><td><code>CODEX_HOME</code></td><td>Replaces the Codex home directory used for detection and for reading an existing Codex memory store.</td><td>A non-default Codex installation, or testing an import without touching real files.</td></tr>
<tr><td><code>PI_CODING_AGENT_DIR</code></td><td>pi's own variable for its agent directory, which Synapse reads for the same reason: detection, the instruction pointer, and the skills folder.</td><td>A non-default pi installation.</td></tr>
<tr><td><code>SYNAPSE_PI_PACKAGE</code></td><td>Replaces the package a connection to pi installs, and the source detection compares against. Defaults to <code>npm:synapse-pi</code>.</td><td>Working on the package from a checkout instead of npm.</td></tr>
Expand Down
Loading