use anyhow::Result;
use rusqlite::{params, Connection, OptionalExtension};
use serde::Serialize;
pub const PER_DESK: i64 = 4;
pub const NOTE_CHARS: usize = 200;
pub const NOTES_PER_DESK: i64 = 200;
const MIN_FRACTION: f64 = 0.15;
const MAX_FRACTION: f64 = 0.85;
pub const SCHEMA: &str = r#"
CREATE TABLE IF NOT EXISTS desks (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
root TEXT NOT NULL,
col REAL NOT NULL DEFAULT 0.5,
row REAL NOT NULL DEFAULT 0.5,
created_at INTEGER NOT NULL,
full_slot INTEGER NOT NULL DEFAULT 0,
closed_at INTEGER NOT NULL DEFAULT 0,
left_off TEXT NOT NULL DEFAULT '',
left_off_at INTEGER NOT NULL DEFAULT 0,
left_off_by TEXT NOT NULL DEFAULT '',
left_off_about TEXT NOT NULL DEFAULT '',
visited_at INTEGER NOT NULL DEFAULT 0,
parked_at INTEGER NOT NULL DEFAULT 0,
parked_next TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS panes (
id TEXT PRIMARY KEY,
desk_id INTEGER NOT NULL REFERENCES desks(id) ON DELETE CASCADE,
slot INTEGER NOT NULL,
cwd TEXT NOT NULL,
cmd TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
agent_session TEXT NOT NULL DEFAULT '',
resume_next INTEGER NOT NULL DEFAULT 0,
name TEXT NOT NULL DEFAULT '',
UNIQUE(desk_id, slot)
);
CREATE INDEX IF NOT EXISTS panes_desk ON panes(desk_id, slot);
CREATE TABLE IF NOT EXISTS panes_closed (
id TEXT PRIMARY KEY,
desk_id INTEGER NOT NULL REFERENCES desks(id) ON DELETE CASCADE,
cwd TEXT NOT NULL,
cmd TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL DEFAULT '',
agent_session TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
closed_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS desk_notes (
id INTEGER PRIMARY KEY,
desk_id INTEGER NOT NULL REFERENCES desks(id) ON DELETE CASCADE,
text TEXT NOT NULL,
done_at INTEGER NOT NULL DEFAULT 0,
removed_at INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
done_by TEXT NOT NULL DEFAULT '',
done_commit TEXT NOT NULL DEFAULT '',
done_doc TEXT NOT NULL DEFAULT '',
done_evidence TEXT NOT NULL DEFAULT '',
suggested_by TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS desk_notes_desk ON desk_notes(desk_id, done_at, id);
"#;
#[derive(Clone, Debug, Serialize)]
pub struct Desk {
pub id: i64,
pub name: String,
pub root: String,
pub col: f64,
pub row: f64,
pub created_at: i64,
pub full_slot: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub left_off: Option<LeftOff>,
pub visited_at: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub parked: Option<Parked>,
pub panes: Vec<Pane>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, serde::Deserialize)]
pub struct Parked {
pub at: i64,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub next: String,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Done {
pub desk_id: i64,
pub text: String,
pub at: i64,
#[serde(skip_serializing_if = "String::is_empty")]
pub by: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub commit: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub doc: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub evidence: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, serde::Deserialize)]
#[serde(default)]
pub struct LeftOff {
pub text: String,
pub at: i64,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub by: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub about: String,
}
pub const LEFT_OFF_CHARS: usize = 200;
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct DeskNote {
pub id: i64,
pub text: String,
pub done: bool,
pub created_at: i64,
#[serde(skip_serializing_if = "String::is_empty")]
pub done_by: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub done_commit: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub done_doc: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub done_evidence: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub suggested_by: String,
}
#[derive(Clone, Debug, Serialize)]
pub struct Pane {
pub id: String,
pub slot: i64,
pub cwd: String,
pub cmd: String,
pub created_at: i64,
pub agent_session: String,
pub name: String,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Origin {
pub id: i64,
pub name: String,
pub slot: i64,
}
#[derive(Clone, Debug)]
pub struct Placed {
pub pane: Pane,
pub desk_id: i64,
pub desk_name: String,
pub root: String,
}
#[derive(Debug)]
pub enum Opened {
Pane(Pane),
DeskFull,
NoSuchDesk,
}
pub const NAME_CHARS: usize = 80;
pub fn list(conn: &Connection) -> Result<Vec<Desk>> {
let mut stmt = conn.prepare(&format!(
"SELECT {DESK_COLS} FROM desks WHERE closed_at = 0 ORDER BY id"
))?;
let mut desks: Vec<Desk> = stmt
.query_map([], row_to_desk)?
.collect::<rusqlite::Result<_>>()?;
let mut stmt = conn.prepare(
"SELECT desk_id, id, slot, cwd, cmd, created_at, agent_session, name FROM panes ORDER BY desk_id, slot",
)?;
let panes: Vec<(i64, Pane)> = stmt
.query_map([], |r| Ok((r.get(0)?, row_to_pane(r, 1)?)))?
.collect::<rusqlite::Result<_>>()?;
for (desk_id, pane) in panes {
if let Some(d) = desks.iter_mut().find(|d| d.id == desk_id) {
d.panes.push(pane);
}
}
Ok(desks)
}
pub fn get(conn: &Connection, id: i64) -> Result<Option<Desk>> {
let Some(mut desk) = conn
.query_row(
&format!("SELECT {DESK_COLS} FROM desks WHERE id = ?1 AND closed_at = 0"),
params![id],
row_to_desk,
)
.optional()?
else {
return Ok(None);
};
let mut stmt = conn.prepare(
"SELECT id, slot, cwd, cmd, created_at, agent_session, name FROM panes WHERE desk_id = ?1 ORDER BY slot",
)?;
desk.panes = stmt
.query_map(params![id], |r| row_to_pane(r, 0))?
.collect::<rusqlite::Result<_>>()?;
Ok(Some(desk))
}
pub fn create(conn: &Connection, root: &str, name: Option<&str>, now: i64) -> Result<Desk> {
let wanted = name
.map(str::trim)
.filter(|n| !n.is_empty())
.map(str::to_string)
.unwrap_or_else(|| derive_name(root));
let name = free_name(conn, &wanted)?;
conn.execute(
"INSERT INTO desks(name, root, col, row, created_at) VALUES(?1, ?2, 0.5, 0.5, ?3)",
params![name, root, now],
)?;
let id = conn.last_insert_rowid();
Ok(Desk {
id,
name,
root: root.to_string(),
col: 0.5,
row: 0.5,
created_at: now,
full_slot: 0,
left_off: None,
visited_at: 0,
parked: None,
panes: Vec::new(),
})
}
pub fn visit(conn: &Connection, id: i64, now: i64) -> Result<bool> {
Ok(conn.execute(
"UPDATE desks SET visited_at = ?2 WHERE id = ?1 AND closed_at = 0 AND visited_at < ?2 - 60",
params![id, now],
)? > 0)
}
pub fn park(conn: &Connection, id: i64, to: Option<&Parked>) -> Result<Option<Option<Parked>>> {
let Some(desk) = get(conn, id)? else {
return Ok(None);
};
let (at, next) = match to {
Some(p) => (p.at, clip_to(&p.next, LEFT_OFF_CHARS)),
None => (0, String::new()),
};
conn.execute(
"UPDATE desks SET parked_at = ?2, parked_next = ?3 WHERE id = ?1",
params![id, at, next],
)?;
Ok(Some(desk.parked))
}
pub fn done_since(conn: &Connection, since: i64) -> Result<Vec<Done>> {
let mut stmt = conn.prepare(
"SELECT n.desk_id, n.text, n.done_at, n.done_by, n.done_commit, n.done_doc, n.done_evidence
FROM desk_notes n JOIN desks d ON d.id = n.desk_id
WHERE n.done_at >= ?1 AND n.done_at != 0 AND n.removed_at = 0 AND d.closed_at = 0
ORDER BY n.done_at, n.id",
)?;
let rows = stmt
.query_map(params![since], |r| {
Ok(Done {
desk_id: r.get(0)?,
text: r.get(1)?,
at: r.get(2)?,
by: r.get(3)?,
commit: r.get(4)?,
doc: r.get(5)?,
evidence: r.get(6)?,
})
})?
.collect::<rusqlite::Result<_>>()?;
Ok(rows)
}
pub fn rename(conn: &Connection, id: i64, name: &str) -> Result<bool> {
let name = name.trim();
if name.is_empty() {
return Ok(false);
}
Ok(conn.execute(
"UPDATE desks SET name = ?2 WHERE id = ?1",
params![id, name],
)? > 0)
}
pub fn layout(conn: &Connection, id: i64, col: f64, row: f64, full: Option<i64>) -> Result<bool> {
let clamp = |f: f64| {
if f.is_finite() {
f.clamp(MIN_FRACTION, MAX_FRACTION)
} else {
0.5
}
};
let full = full.map(|s| if (1..=PER_DESK).contains(&s) { s } else { 0 });
Ok(conn.execute(
"UPDATE desks SET col = ?2, row = ?3, full_slot = COALESCE(?4, full_slot) WHERE id = ?1",
params![id, clamp(col), clamp(row), full],
)? > 0)
}
pub fn close(conn: &mut Connection, id: i64, now: i64) -> Result<Option<Vec<String>>> {
let tx = conn.transaction()?;
if tx.execute(
"UPDATE desks SET closed_at = ?2 WHERE id = ?1 AND closed_at = 0",
params![id, now],
)? == 0
{
return Ok(None);
}
let ids: Vec<String> = tx
.prepare("SELECT id FROM panes WHERE desk_id = ?1 ORDER BY slot")?
.query_map(params![id], |r| r.get(0))?
.collect::<rusqlite::Result<_>>()?;
for p in &ids {
tx.execute(
"INSERT OR REPLACE INTO panes_closed(id, desk_id, cwd, cmd, name, agent_session, created_at, closed_at)
SELECT id, desk_id, cwd, cmd, name, agent_session, created_at, ?2 FROM panes WHERE id = ?1",
params![p, now],
)?;
}
tx.execute("DELETE FROM panes WHERE desk_id = ?1", params![id])?;
tx.commit()?;
Ok(Some(ids))
}
pub fn reopen(conn: &mut Connection, id: i64) -> Result<bool> {
let tx = conn.transaction()?;
let Some(at) = tx
.query_row(
"SELECT closed_at FROM desks WHERE id = ?1 AND closed_at != 0",
params![id],
|r| r.get::<_, i64>(0),
)
.optional()?
else {
return Ok(false);
};
tx.execute("UPDATE desks SET closed_at = 0 WHERE id = ?1", params![id])?;
let back: Vec<String> = tx
.prepare(
"SELECT id FROM panes_closed WHERE desk_id = ?1 AND closed_at = ?2
ORDER BY rowid LIMIT ?3",
)?
.query_map(params![id, at, PER_DESK], |r| r.get(0))?
.collect::<rusqlite::Result<_>>()?;
for (i, p) in back.iter().enumerate() {
tx.execute(
"INSERT INTO panes(id, desk_id, slot, cwd, cmd, name, agent_session, created_at)
SELECT id, desk_id, ?2, cwd, cmd, name, agent_session, created_at FROM panes_closed WHERE id = ?1",
params![p, i as i64 + 1],
)?;
tx.execute("DELETE FROM panes_closed WHERE id = ?1", params![p])?;
}
tx.commit()?;
Ok(true)
}
pub type ClosedDesk = (i64, String, String, i64);
pub fn closed_desks(conn: &Connection, limit: usize) -> Result<Vec<ClosedDesk>> {
let mut stmt = conn.prepare(
"SELECT id, name, root, closed_at FROM desks WHERE closed_at != 0 ORDER BY closed_at DESC LIMIT ?1",
)?;
let rows = stmt
.query_map(params![limit as i64], |r| {
Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?))
})?
.collect::<rusqlite::Result<_>>()?;
Ok(rows)
}
pub fn prune_desks(conn: &Connection, before: i64, dry_run: bool) -> Result<Vec<(i64, String)>> {
let mut stmt = conn.prepare(
"SELECT id, name FROM desks WHERE closed_at != 0 AND closed_at < ?1 ORDER BY closed_at",
)?;
let gone: Vec<(i64, String)> = stmt
.query_map(params![before], |r| Ok((r.get(0)?, r.get(1)?)))?
.collect::<rusqlite::Result<_>>()?;
if !dry_run {
conn.execute(
"DELETE FROM desks WHERE closed_at != 0 AND closed_at < ?1",
params![before],
)?;
}
Ok(gone)
}
pub fn open_pane(
conn: &mut Connection,
desk_id: i64,
cwd: &str,
cmd: &str,
now: i64,
) -> Result<Opened> {
let tx = conn.transaction()?;
let exists: bool = tx
.query_row(
"SELECT 1 FROM desks WHERE id = ?1 AND closed_at = 0",
params![desk_id],
|_| Ok(()),
)
.optional()?
.is_some();
if !exists {
return Ok(Opened::NoSuchDesk);
}
let taken: Vec<i64> = tx
.prepare("SELECT slot FROM panes WHERE desk_id = ?1")?
.query_map(params![desk_id], |r| r.get(0))?
.collect::<rusqlite::Result<_>>()?;
let Some(slot) = (1..=PER_DESK).find(|s| !taken.contains(s)) else {
return Ok(Opened::DeskFull);
};
let id = pane_id()?;
tx.execute(
"INSERT INTO panes(id, desk_id, slot, cwd, cmd, created_at) VALUES(?1, ?2, ?3, ?4, ?5, ?6)",
params![id, desk_id, slot, cwd, cmd, now],
)?;
tx.commit()?;
Ok(Opened::Pane(Pane {
id,
slot,
cwd: cwd.to_string(),
cmd: cmd.to_string(),
created_at: now,
agent_session: String::new(),
name: String::new(),
}))
}
pub fn move_pane(tx: &rusqlite::Transaction, desk_id: i64, from: i64, to: i64) -> Result<bool> {
let range = 1..=PER_DESK;
if !range.contains(&from) || !range.contains(&to) {
return Ok(false);
}
let moving: Option<String> = tx
.query_row(
"SELECT id FROM panes WHERE desk_id = ?1 AND slot = ?2",
params![desk_id, from],
|r| r.get(0),
)
.optional()?;
let Some(moving) = moving else {
return Ok(false);
};
if from == to {
return Ok(true);
}
tx.execute("UPDATE panes SET slot = 0 WHERE id = ?1", params![moving])?;
tx.execute(
"UPDATE panes SET slot = ?3 WHERE desk_id = ?1 AND slot = ?2",
params![desk_id, to, from],
)?;
tx.execute(
"UPDATE panes SET slot = ?2 WHERE id = ?1",
params![moving, to],
)?;
tx.execute(
"UPDATE desks SET full_slot = CASE full_slot WHEN ?2 THEN ?3 WHEN ?3 THEN ?2 ELSE full_slot END WHERE id = ?1",
params![desk_id, from, to],
)?;
Ok(true)
}
pub fn close_pane(tx: &rusqlite::Transaction, id: &str, now: i64) -> Result<Option<(i64, i64)>> {
let Some((desk_id, slot)) = tx
.query_row(
"SELECT desk_id, slot FROM panes WHERE id = ?1",
params![id],
|r| Ok((r.get::<_, i64>(0)?, r.get::<_, i64>(1)?)),
)
.optional()?
else {
return Ok(None);
};
tx.execute(
"INSERT OR REPLACE INTO panes_closed(id, desk_id, cwd, cmd, name, agent_session, created_at, closed_at)
SELECT id, desk_id, cwd, cmd, name, agent_session, created_at, ?2 FROM panes WHERE id = ?1",
params![id, now],
)?;
tx.execute("DELETE FROM panes WHERE id = ?1", params![id])?;
for s in slot + 1..=PER_DESK {
tx.execute(
"UPDATE panes SET slot = ?3 WHERE desk_id = ?1 AND slot = ?2",
params![desk_id, s, s - 1],
)?;
}
tx.execute(
"UPDATE desks SET full_slot = CASE WHEN full_slot = ?2 THEN 0 WHEN full_slot > ?2 THEN full_slot - 1 ELSE full_slot END WHERE id = ?1",
params![desk_id, slot],
)?;
Ok(Some((desk_id, slot)))
}
#[derive(Debug)]
pub enum Restored {
Pane(Pane),
DeskFull,
Gone,
}
pub fn restore_pane(conn: &mut Connection, id: &str) -> Result<Restored> {
let tx = conn.transaction()?;
let Some(desk_id) = tx
.query_row(
"SELECT desk_id FROM panes_closed WHERE id = ?1",
params![id],
|r| r.get::<_, i64>(0),
)
.optional()?
else {
return Ok(Restored::Gone);
};
let taken: Vec<i64> = tx
.prepare("SELECT slot FROM panes WHERE desk_id = ?1")?
.query_map(params![desk_id], |r| r.get(0))?
.collect::<rusqlite::Result<_>>()?;
let Some(slot) = (1..=PER_DESK).find(|s| !taken.contains(s)) else {
return Ok(Restored::DeskFull);
};
tx.execute(
"INSERT INTO panes(id, desk_id, slot, cwd, cmd, name, agent_session, created_at)
SELECT id, desk_id, ?2, cwd, cmd, name, agent_session, created_at FROM panes_closed WHERE id = ?1",
params![id, slot],
)?;
tx.execute("DELETE FROM panes_closed WHERE id = ?1", params![id])?;
let pane = tx.query_row(
"SELECT id, slot, cwd, cmd, created_at, agent_session, name FROM panes WHERE id = ?1",
params![id],
|r| row_to_pane(r, 0),
)?;
tx.commit()?;
Ok(Restored::Pane(pane))
}
#[cfg(test)]
fn closed_on(conn: &Connection, desk_id: i64) -> Result<Vec<String>> {
let mut stmt = conn.prepare("SELECT id FROM panes_closed WHERE desk_id = ?1")?;
let ids = stmt
.query_map(params![desk_id], |r| r.get(0))?
.collect::<rusqlite::Result<_>>()?;
Ok(ids)
}
pub fn prune_closed(
conn: &Connection,
before: i64,
dry_run: bool,
) -> Result<Vec<(String, String)>> {
let mut stmt = conn.prepare(
"SELECT c.id, COALESCE(NULLIF(c.name, ''), NULLIF(c.cmd, ''), 'shell') || ' on ' || d.name
FROM panes_closed c JOIN desks d ON d.id = c.desk_id WHERE c.closed_at < ?1 ORDER BY c.closed_at",
)?;
let gone: Vec<(String, String)> = stmt
.query_map(params![before], |r| Ok((r.get(0)?, r.get(1)?)))?
.collect::<rusqlite::Result<_>>()?;
if !dry_run {
conn.execute(
"DELETE FROM panes_closed WHERE closed_at < ?1",
params![before],
)?;
}
Ok(gone)
}
pub fn rename_pane(conn: &Connection, id: &str, name: &str) -> Result<bool> {
let name: String = name
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.chars()
.take(NAME_CHARS)
.collect();
Ok(conn.execute(
"UPDATE panes SET name = ?2 WHERE id = ?1",
params![id, name],
)? > 0)
}
pub fn pane(conn: &Connection, id: &str) -> Result<Option<Placed>> {
Ok(conn
.query_row(
"SELECT p.id, p.slot, p.cwd, p.cmd, p.created_at, p.agent_session, p.name, d.id, d.name, d.root
FROM panes p JOIN desks d ON d.id = p.desk_id WHERE p.id = ?1",
params![id],
|r| {
Ok(Placed {
pane: row_to_pane(r, 0)?,
desk_id: r.get(7)?,
desk_name: r.get(8)?,
root: r.get(9)?,
})
},
)
.optional()?)
}
pub fn set_cmd(conn: &Connection, id: &str, cmd: &str) -> Result<bool> {
Ok(conn.execute(
"UPDATE panes SET cmd = ?2 WHERE id = ?1",
params![id, cmd.trim()],
)? > 0)
}
pub fn set_agent_session(conn: &Connection, id: &str, session: &str) -> Result<bool> {
if !valid_session(session) {
return Ok(false);
}
Ok(conn.execute(
"UPDATE panes SET agent_session = ?2 WHERE id = ?1 AND agent_session <> ?2",
params![id, session],
)? > 0)
}
pub fn mark_resume(conn: &Connection, ids: &[String]) -> Result<usize> {
conn.execute(
"UPDATE panes SET resume_next = 0 WHERE resume_next <> 0",
[],
)?;
let mut n = 0;
for id in ids {
n += conn.execute(
"UPDATE panes SET resume_next = 1 WHERE id = ?1 AND agent_session <> ''",
params![id],
)?;
}
Ok(n)
}
pub fn mark_offer(conn: &Connection, ids: &[String]) -> Result<usize> {
let mut n = 0;
for id in ids {
n += conn.execute(
"UPDATE panes SET resume_next = 2 WHERE id = ?1 AND resume_next = 0 AND agent_session <> ''",
params![id],
)?;
}
Ok(n)
}
pub fn read_resume(conn: &Connection) -> Result<(Vec<String>, Vec<String>)> {
let mut stmt =
conn.prepare("SELECT id, resume_next FROM panes WHERE resume_next <> 0 ORDER BY id")?;
let rows: Vec<(String, i64)> = stmt
.query_map([], |r| Ok((r.get(0)?, r.get(1)?)))?
.collect::<rusqlite::Result<_>>()?;
let (planned, offered): (Vec<_>, Vec<_>) = rows.into_iter().partition(|(_, k)| *k == 1);
Ok((
planned.into_iter().map(|(id, _)| id).collect(),
offered.into_iter().map(|(id, _)| id).collect(),
))
}
pub fn clear_resume(conn: &Connection) -> Result<()> {
conn.execute(
"UPDATE panes SET resume_next = 0 WHERE resume_next <> 0",
[],
)?;
Ok(())
}
#[cfg(test)]
fn take_resume(conn: &Connection) -> Result<(Vec<String>, Vec<String>)> {
let marks = read_resume(conn)?;
clear_resume(conn)?;
Ok(marks)
}
pub fn set_cwd(conn: &Connection, id: &str, cwd: &str) -> Result<bool> {
Ok(conn.execute(
"UPDATE panes SET cwd = ?2 WHERE id = ?1 AND cwd <> ?2",
params![id, cwd],
)? > 0)
}
pub fn valid_session(s: &str) -> bool {
s.len() == 36
&& s.bytes().enumerate().all(|(i, b)| match i {
8 | 13 | 18 | 23 => b == b'-',
_ => b.is_ascii_digit() || (b'a'..=b'f').contains(&b),
})
}
pub fn panes_open(conn: &Connection) -> Result<i64> {
Ok(conn.query_row("SELECT COUNT(*) FROM panes", [], |r| r.get(0))?)
}
pub fn clear(conn: &Connection) -> Result<()> {
conn.execute_batch(
"DELETE FROM desk_notes; DELETE FROM panes_closed; DELETE FROM panes; DELETE FROM desks;",
)?;
Ok(())
}
pub fn notes(conn: &Connection, desk_id: i64) -> Result<Vec<DeskNote>> {
let mut stmt = conn.prepare(
"SELECT id, text, done_at, created_at, done_by, done_commit, done_doc, done_evidence, suggested_by FROM desk_notes
WHERE desk_id = ?1 AND removed_at = 0
ORDER BY CASE WHEN done_at != 0 THEN 2 WHEN suggested_by != '' THEN 1 ELSE 0 END, done_at, id",
)?;
let notes: Vec<DeskNote> = stmt
.query_map(params![desk_id], row_to_note)?
.collect::<rusqlite::Result<_>>()?;
Ok(notes)
}
pub fn add_note(
conn: &mut Connection,
desk_id: i64,
text: &str,
now: i64,
) -> Result<Option<DeskNote>> {
let text = clip(text);
if text.is_empty() {
return Ok(None);
}
let tx = conn.transaction()?;
let exists: bool = tx
.query_row(
"SELECT 1 FROM desks WHERE id = ?1 AND closed_at = 0",
params![desk_id],
|_| Ok(()),
)
.optional()?
.is_some();
if !exists {
return Ok(None);
}
let held: i64 = tx.query_row(
"SELECT COUNT(*) FROM desk_notes WHERE desk_id = ?1 AND removed_at = 0",
params![desk_id],
|r| r.get(0),
)?;
if held >= NOTES_PER_DESK {
return Ok(None);
}
tx.execute(
"INSERT INTO desk_notes(desk_id, text, created_at) VALUES(?1, ?2, ?3)",
params![desk_id, text, now],
)?;
let id = tx.last_insert_rowid();
tx.commit()?;
Ok(Some(DeskNote {
id,
text,
done: false,
created_at: now,
done_by: String::new(),
done_commit: String::new(),
done_doc: String::new(),
done_evidence: String::new(),
suggested_by: String::new(),
}))
}
pub fn set_note(
conn: &Connection,
desk_id: i64,
id: i64,
text: Option<&str>,
done: Option<bool>,
now: i64,
) -> Result<bool> {
if let Some(text) = text {
let text = clip(text);
if text.is_empty() {
return remove_note(conn, desk_id, id, now);
}
if conn.execute(
"UPDATE desk_notes SET text = ?3 WHERE desk_id = ?1 AND id = ?2 AND removed_at = 0",
params![desk_id, id, text],
)? == 0
{
return Ok(false);
}
}
let Some(done) = done else {
return Ok(text.is_some());
};
Ok(conn.execute(
"UPDATE desk_notes SET done_at = ?3, done_by = '', done_commit = '', done_doc = '', done_evidence = '', suggested_by = ''
WHERE desk_id = ?1 AND id = ?2 AND removed_at = 0",
params![desk_id, id, if done { now } else { 0 }],
)? > 0)
}
#[derive(Clone, Debug, Default)]
pub struct Tick {
pub by: String,
pub commit: String,
pub doc: String,
pub evidence: String,
}
pub fn commit_ok(commit: &str) -> bool {
(7..=40).contains(&commit.len()) && commit.bytes().all(|b| b.is_ascii_hexdigit())
}
pub fn doc_ok(doc: &str) -> bool {
doc.len() == 10 && doc.bytes().all(|b| b.is_ascii_hexdigit())
}
pub fn evidence_ok(url: &str) -> bool {
let rest = url
.strip_prefix("https://")
.or_else(|| url.strip_prefix("http://"));
url.len() <= 500
&& rest.is_some_and(|r| !r.is_empty() && !r.starts_with('/') && !r.starts_with('.'))
&& url.bytes().all(|b| b.is_ascii_graphic())
}
pub fn tick_note(conn: &Connection, desk_id: i64, id: i64, tick: &Tick, now: i64) -> Result<bool> {
let by = if tick.by.trim().is_empty() {
"an agent"
} else {
tick.by.trim()
};
let by: String = by.chars().take(60).collect();
let commit = if commit_ok(&tick.commit) {
tick.commit.to_ascii_lowercase()
} else {
String::new()
};
let doc = if doc_ok(&tick.doc) {
tick.doc.to_ascii_lowercase()
} else {
String::new()
};
let evidence = if evidence_ok(&tick.evidence) {
tick.evidence.clone()
} else {
String::new()
};
Ok(conn.execute(
"UPDATE desk_notes SET done_at = ?3, done_by = ?4, done_commit = ?5, done_doc = ?6, done_evidence = ?7
WHERE desk_id = ?1 AND id = ?2 AND removed_at = 0 AND done_at = 0 AND suggested_by = ''",
params![desk_id, id, now, by, commit, doc, evidence],
)? > 0)
}
pub const SUGGESTIONS_PER_DESK: i64 = 3;
#[derive(Debug, PartialEq)]
pub enum Suggested {
Note(DeskNote),
Full,
Empty,
NoSuchDesk,
}
pub fn suggest_note(
conn: &mut Connection,
desk_id: i64,
text: &str,
by: &str,
now: i64,
) -> Result<Suggested> {
let text = clip(text);
if text.is_empty() {
return Ok(Suggested::Empty);
}
let by = if by.trim().is_empty() {
"an agent"
} else {
by.trim()
};
let by: String = by.chars().take(60).collect();
let tx = conn.transaction()?;
if tx
.query_row(
"SELECT 1 FROM desks WHERE id = ?1 AND closed_at = 0",
params![desk_id],
|_| Ok(()),
)
.optional()?
.is_none()
{
return Ok(Suggested::NoSuchDesk);
}
let (held, waiting): (i64, i64) = tx.query_row(
"SELECT COUNT(*), COALESCE(SUM(suggested_by != '' AND done_at = 0), 0) FROM desk_notes
WHERE desk_id = ?1 AND removed_at = 0",
params![desk_id],
|r| Ok((r.get(0)?, r.get(1)?)),
)?;
if held >= NOTES_PER_DESK || waiting >= SUGGESTIONS_PER_DESK {
return Ok(Suggested::Full);
}
tx.execute(
"INSERT INTO desk_notes(desk_id, text, created_at, suggested_by) VALUES(?1, ?2, ?3, ?4)",
params![desk_id, text, now, by],
)?;
let id = tx.last_insert_rowid();
tx.commit()?;
Ok(Suggested::Note(DeskNote {
id,
text,
done: false,
created_at: now,
done_by: String::new(),
done_commit: String::new(),
done_doc: String::new(),
done_evidence: String::new(),
suggested_by: by,
}))
}
pub fn keep_note(conn: &Connection, desk_id: i64, id: i64) -> Result<bool> {
Ok(conn.execute(
"UPDATE desk_notes SET suggested_by = '' WHERE desk_id = ?1 AND id = ?2 AND removed_at = 0 AND suggested_by != ''",
params![desk_id, id],
)? > 0)
}
pub fn set_left_off(
conn: &Connection,
desk_id: i64,
to: &LeftOff,
) -> Result<Option<Option<LeftOff>>> {
let text = clip_to(&to.text, LEFT_OFF_CHARS);
let by: String = to.by.trim().chars().take(60).collect();
let about = if doc_ok(&to.about) {
to.about.to_ascii_lowercase()
} else {
String::new()
};
let Some(desk) = get(conn, desk_id)? else {
return Ok(None);
};
let (at, by, about) = if text.is_empty() {
(0, String::new(), String::new())
} else {
(to.at, by, about)
};
conn.execute(
"UPDATE desks SET left_off = ?2, left_off_at = ?3, left_off_by = ?4, left_off_about = ?5 WHERE id = ?1",
params![desk_id, text, at, by, about],
)?;
Ok(Some(desk.left_off))
}
pub fn remove_note(conn: &Connection, desk_id: i64, id: i64, now: i64) -> Result<bool> {
Ok(conn.execute(
"UPDATE desk_notes SET removed_at = ?3 WHERE desk_id = ?1 AND id = ?2 AND removed_at = 0",
params![desk_id, id, now],
)? > 0)
}
pub type RemovedNote = (i64, i64, String, String, i64);
pub type ClosedPane = (String, i64, String, String, i64);
pub fn removed_notes(conn: &Connection, limit: usize) -> Result<Vec<RemovedNote>> {
let mut stmt = conn.prepare(
"SELECT n.desk_id, n.id, d.name, n.text, n.removed_at FROM desk_notes n JOIN desks d ON d.id = n.desk_id
WHERE n.removed_at != 0 AND d.closed_at = 0 ORDER BY n.removed_at DESC, n.id DESC LIMIT ?1",
)?;
let rows = stmt
.query_map(params![limit as i64], |r| {
Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?))
})?
.collect::<rusqlite::Result<_>>()?;
Ok(rows)
}
pub fn closed_panes(conn: &Connection, limit: usize) -> Result<Vec<ClosedPane>> {
let mut stmt = conn.prepare(
"SELECT c.id, c.desk_id, d.name, COALESCE(NULLIF(c.name, ''), NULLIF(c.cmd, ''), 'shell'), c.closed_at
FROM panes_closed c JOIN desks d ON d.id = c.desk_id WHERE d.closed_at = 0 ORDER BY c.closed_at DESC LIMIT ?1",
)?;
let rows = stmt
.query_map(params![limit as i64], |r| {
Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?))
})?
.collect::<rusqlite::Result<_>>()?;
Ok(rows)
}
pub fn restore_note(conn: &Connection, desk_id: i64, id: i64) -> Result<bool> {
Ok(conn.execute(
"UPDATE desk_notes SET removed_at = 0 WHERE desk_id = ?1 AND id = ?2",
params![desk_id, id],
)? > 0)
}
fn clip(text: &str) -> String {
let text = text.trim();
match text.char_indices().nth(NOTE_CHARS) {
Some((at, _)) => text[..at].trim_end().to_string(),
None => text.to_string(),
}
}
fn clip_to(text: &str, chars: usize) -> String {
let text = text.split_whitespace().collect::<Vec<_>>().join(" ");
match text.char_indices().nth(chars) {
Some((at, _)) => text[..at].trim_end().to_string(),
None => text,
}
}
fn row_to_note(r: &rusqlite::Row) -> rusqlite::Result<DeskNote> {
Ok(DeskNote {
id: r.get(0)?,
text: r.get(1)?,
done: r.get::<_, i64>(2)? != 0,
created_at: r.get(3)?,
done_by: r.get(4)?,
done_commit: r.get(5)?,
done_doc: r.get(6)?,
done_evidence: r.get(7)?,
suggested_by: r.get(8)?,
})
}
fn derive_name(root: &str) -> String {
std::path::Path::new(root)
.file_name()
.map(|n| n.to_string_lossy().to_string())
.filter(|n| !n.is_empty())
.unwrap_or_else(|| "desk".to_string())
}
fn free_name(conn: &Connection, wanted: &str) -> Result<String> {
let taken = |name: &str| -> Result<bool> {
Ok(conn
.query_row(
"SELECT 1 FROM desks WHERE name = ?1 AND closed_at = 0",
params![name],
|_| Ok(()),
)
.optional()?
.is_some())
};
if !taken(wanted)? {
return Ok(wanted.to_string());
}
for n in 2.. {
let candidate = format!("{wanted} {n}");
if !taken(&candidate)? {
return Ok(candidate);
}
}
unreachable!("the range is unbounded")
}
fn pane_id() -> Result<String> {
let mut buf = [0u8; 16];
getrandom::fill(&mut buf)
.map_err(|e| anyhow::anyhow!("reading random bytes for a pane id: {e}"))?;
Ok(buf.iter().map(|b| format!("{b:02x}")).collect())
}
const DESK_COLS: &str =
"id, name, root, col, row, created_at, full_slot, left_off, left_off_at, left_off_by, left_off_about, visited_at, parked_at, parked_next";
fn row_to_desk(r: &rusqlite::Row) -> rusqlite::Result<Desk> {
let text: String = r.get(7)?;
Ok(Desk {
id: r.get(0)?,
name: r.get(1)?,
root: r.get(2)?,
col: r.get(3)?,
row: r.get(4)?,
created_at: r.get(5)?,
full_slot: r.get(6)?,
left_off: if text.is_empty() {
None
} else {
Some(LeftOff {
text,
at: r.get(8)?,
by: r.get(9)?,
about: r.get(10)?,
})
},
visited_at: r.get(11)?,
parked: match r.get::<_, i64>(12)? {
0 => None,
at => Some(Parked {
at,
next: r.get(13)?,
}),
},
panes: Vec::new(),
})
}
fn row_to_pane(r: &rusqlite::Row, at: usize) -> rusqlite::Result<Pane> {
Ok(Pane {
id: r.get(at)?,
slot: r.get(at + 1)?,
cwd: r.get(at + 2)?,
cmd: r.get(at + 3)?,
created_at: r.get(at + 4)?,
agent_session: r.get(at + 5)?,
name: r.get(at + 6)?,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn db() -> Connection {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap();
conn.execute_batch(SCHEMA).unwrap();
conn
}
#[test]
fn a_list_reads_open_first_then_done_in_the_order_it_was_ticked() {
let mut conn = db();
let d = create(&conn, "/w", None, 0).unwrap().id;
for (i, text) in ["first", "second", "third"].iter().enumerate() {
add_note(&mut conn, d, text, i as i64).unwrap().unwrap();
}
let ids: Vec<i64> = notes(&conn, d).unwrap().iter().map(|n| n.id).collect();
set_note(&conn, d, ids[1], None, Some(true), 10).unwrap();
set_note(&conn, d, ids[0], None, Some(true), 20).unwrap();
let after = notes(&conn, d).unwrap();
assert_eq!(
after
.iter()
.map(|n| (n.text.as_str(), n.done))
.collect::<Vec<_>>(),
[("third", false), ("second", true), ("first", true)]
);
set_note(&conn, d, ids[0], None, Some(false), 30).unwrap();
assert_eq!(notes(&conn, d).unwrap()[0].text, "first");
}
#[test]
fn a_line_taken_off_is_kept_and_can_come_back() {
let mut conn = db();
let d = create(&conn, "/w", None, 0).unwrap().id;
let n = add_note(&mut conn, d, "wire up the route", 0)
.unwrap()
.unwrap();
assert!(remove_note(&conn, d, n.id, 1).unwrap());
assert!(notes(&conn, d).unwrap().is_empty());
let kept: i64 = conn
.query_row("SELECT COUNT(*) FROM desk_notes", [], |r| r.get(0))
.unwrap();
assert_eq!(kept, 1, "the row is kept, not deleted");
assert!(restore_note(&conn, d, n.id).unwrap());
assert_eq!(notes(&conn, d).unwrap()[0].text, "wire up the route");
assert!(remove_note(&conn, d, n.id, 2).unwrap());
assert!(!remove_note(&conn, d, n.id, 3).unwrap());
}
#[test]
fn a_note_is_reachable_only_through_its_own_desk() {
let mut conn = db();
let mine = create(&conn, "/mine", None, 0).unwrap().id;
let yours = create(&conn, "/yours", None, 0).unwrap().id;
let n = add_note(&mut conn, mine, "mine", 0).unwrap().unwrap();
assert!(!set_note(&conn, yours, n.id, Some("yours"), None, 1).unwrap());
assert!(!remove_note(&conn, yours, n.id, 1).unwrap());
assert_eq!(notes(&conn, mine).unwrap()[0].text, "mine");
assert!(notes(&conn, yours).unwrap().is_empty());
}
#[test]
fn an_agent_ticks_an_open_line_on_its_own_desk_and_nothing_else() {
let mut conn = db();
let mine = create(&conn, "/mine", None, 0).unwrap().id;
let yours = create(&conn, "/yours", None, 0).unwrap().id;
let n = add_note(&mut conn, mine, "wire the route", 0)
.unwrap()
.unwrap();
assert!(
!tick_note(&conn, yours, n.id, &by("claude-code"), 1).unwrap(),
"not across desks"
);
assert!(tick_note(&conn, mine, n.id, &by("claude-code"), 1).unwrap());
let got = ¬es(&conn, mine).unwrap()[0];
assert!(got.done);
assert_eq!(got.done_by, "claude-code");
assert!(
!tick_note(&conn, mine, n.id, &by("claude-code"), 2).unwrap(),
"a done line stays as it is"
);
assert!(set_note(&conn, mine, n.id, None, Some(false), 3).unwrap());
let got = ¬es(&conn, mine).unwrap()[0];
assert!(!got.done);
assert_eq!(got.done_by, "");
assert!(tick_note(&conn, mine, n.id, &by(" "), 4).unwrap());
assert_eq!(notes(&conn, mine).unwrap()[0].done_by, "an agent");
assert!(set_note(&conn, mine, n.id, None, Some(false), 5).unwrap());
assert!(remove_note(&conn, mine, n.id, 6).unwrap());
assert!(!tick_note(&conn, mine, n.id, &by("claude-code"), 7).unwrap());
}
fn by(name: &str) -> Tick {
Tick {
by: name.into(),
..Tick::default()
}
}
#[test]
fn a_tick_carries_its_commit_and_document_and_an_untick_clears_them() {
let mut conn = db();
let mine = create(&conn, "/mine", None, 0).unwrap().id;
let n = add_note(&mut conn, mine, "fix the hover", 0)
.unwrap()
.unwrap();
let tick = Tick {
by: "claude-code".into(),
commit: "90F09D6".into(),
doc: "82cc8f2d3c".into(),
evidence: "https://github.com/o/r/pull/40".into(),
};
assert!(tick_note(&conn, mine, n.id, &tick, 1).unwrap());
let got = ¬es(&conn, mine).unwrap()[0];
assert_eq!(
(
got.done_commit.as_str(),
got.done_doc.as_str(),
got.done_evidence.as_str()
),
("90f09d6", "82cc8f2d3c", "https://github.com/o/r/pull/40")
);
assert!(set_note(&conn, mine, n.id, None, Some(false), 2).unwrap());
let got = ¬es(&conn, mine).unwrap()[0];
assert_eq!(
(
got.done_by.as_str(),
got.done_commit.as_str(),
got.done_doc.as_str()
),
("", "", "")
);
let junk = Tick {
by: "claude-code".into(),
commit: "main; rm -rf".into(),
doc: "../etc".into(),
evidence: "javascript:alert(1)".into(),
};
assert!(tick_note(&conn, mine, n.id, &junk, 3).unwrap());
let got = ¬es(&conn, mine).unwrap()[0];
assert_eq!(
(
got.done_commit.as_str(),
got.done_doc.as_str(),
got.done_evidence.as_str()
),
("", "", "")
);
assert!(
evidence_ok("https://apps.apple.com/app/id1") && evidence_ok("http://localhost:3000/x")
);
for bad in [
"file:///etc/passwd",
"https://",
"https:///x",
"https://a b",
"ftp://x",
"https://x\n",
] {
assert!(!evidence_ok(bad), "{bad}");
}
assert!(!evidence_ok(&format!("https://x/{}", "a".repeat(500))));
assert!(commit_ok("90f09d6") && commit_ok(&"a".repeat(40)));
assert!(!commit_ok("90f09d") && !commit_ok(&"a".repeat(41)) && !commit_ok("main"));
assert!(doc_ok("82cc8f2d3c") && !doc_ok("82cc8f2d3") && !doc_ok("82cc8f2d3z"));
}
#[test]
fn a_line_rewritten_to_nothing_comes_off_the_list() {
let mut conn = db();
let d = create(&conn, "/w", None, 0).unwrap().id;
let n = add_note(&mut conn, d, "something", 0).unwrap().unwrap();
assert!(set_note(&conn, d, n.id, Some(" "), None, 1).unwrap());
assert!(notes(&conn, d).unwrap().is_empty());
assert!(restore_note(&conn, d, n.id).unwrap());
assert_eq!(notes(&conn, d).unwrap()[0].text, "something");
}
#[test]
fn a_list_is_bounded_and_takes_no_empty_line() {
let mut conn = db();
let d = create(&conn, "/w", None, 0).unwrap().id;
assert!(add_note(&mut conn, d, " ", 0).unwrap().is_none());
assert!(add_note(&mut conn, d + 99, "nowhere", 0).unwrap().is_none());
for i in 0..NOTES_PER_DESK {
assert!(add_note(&mut conn, d, &format!("line {i}"), i)
.unwrap()
.is_some());
}
assert!(add_note(&mut conn, d, "one too many", 0).unwrap().is_none());
let first = notes(&conn, d).unwrap()[0].id;
remove_note(&conn, d, first, 1).unwrap();
assert!(add_note(&mut conn, d, "room now", 0).unwrap().is_some());
}
#[test]
fn a_long_line_is_cut_where_a_character_ends() {
let mut conn = db();
let d = create(&conn, "/w", None, 0).unwrap().id;
let long = "æ—¥".repeat(NOTE_CHARS + 50);
let n = add_note(&mut conn, d, &long, 0).unwrap().unwrap();
assert_eq!(n.text.chars().count(), NOTE_CHARS);
assert_eq!(notes(&conn, d).unwrap()[0].text, n.text);
}
#[test]
fn closing_a_desk_keeps_its_list_and_reopening_brings_it_back() {
let mut conn = db();
let d = create(&conn, "/w", None, 0).unwrap().id;
let kept = add_note(&mut conn, d, "stays with it", 0).unwrap().unwrap();
add_note(&mut conn, d, "and this", 0).unwrap().unwrap();
assert!(set_note(&conn, d, kept.id, None, Some(true), 5).unwrap());
let (Opened::Pane(a), Opened::Pane(b), Opened::Pane(c)) =
(pane(&mut conn, d), pane(&mut conn, d), pane(&mut conn, d))
else {
panic!()
};
let tx = conn.transaction().unwrap();
close_pane(&tx, &a.id, 10).unwrap();
tx.commit().unwrap();
assert_eq!(
close(&mut conn, d, 20).unwrap(),
Some(vec![b.id.clone(), c.id.clone()])
);
assert_eq!(close(&mut conn, d, 21).unwrap(), None, "already closed");
assert!(list(&conn).unwrap().is_empty());
assert!(get(&conn, d).unwrap().is_none());
assert!(add_note(&mut conn, d, "not on a closed desk", 0)
.unwrap()
.is_none());
assert!(matches!(pane(&mut conn, d), Opened::NoSuchDesk));
assert_eq!(closed_desks(&conn, 10).unwrap().len(), 1);
assert!(
closed_panes(&conn, 10).unwrap().is_empty(),
"the desk's row stands for them"
);
assert_eq!(
create(&conn, "/w", None, 0).unwrap().name,
"w",
"its name is free while it is closed"
);
assert!(reopen(&mut conn, d).unwrap());
assert!(!reopen(&mut conn, d).unwrap(), "already open");
let back = get(&conn, d).unwrap().unwrap();
assert_eq!(
back.panes
.iter()
.map(|p| (p.id.clone(), p.slot))
.collect::<Vec<_>>(),
vec![(b.id.clone(), 1), (c.id.clone(), 2)]
);
assert_eq!(
closed_on(&conn, d).unwrap(),
vec![a.id.clone()],
"closed before it, still closed"
);
let ns = notes(&conn, d).unwrap();
assert_eq!(ns.len(), 2);
assert!(ns.iter().find(|n| n.id == kept.id).unwrap().done);
close(&mut conn, d, 30).unwrap();
assert!(
prune_desks(&conn, 30, false).unwrap().is_empty(),
"not before its close"
);
assert_eq!(prune_desks(&conn, 31, true).unwrap().len(), 1);
assert_eq!(
prune_desks(&conn, 31, false).unwrap(),
vec![(d, "w".to_string())]
);
let left: i64 = conn
.query_row("SELECT COUNT(*) FROM desk_notes", [], |r| r.get(0))
.unwrap();
assert_eq!(left, 0, "a pruned desk takes its list by the cascade");
}
fn pane(conn: &mut Connection, desk: i64) -> Opened {
open_pane(conn, desk, "/p", "", 0).unwrap()
}
fn slots(conn: &Connection, desk: i64) -> Vec<i64> {
get(conn, desk)
.unwrap()
.unwrap()
.panes
.iter()
.map(|p| p.slot)
.collect()
}
#[test]
fn a_pane_keeps_the_last_conversation_and_only_a_uuid() {
let mut conn = db();
let d = create(&conn, "/p", None, 0).unwrap();
let Opened::Pane(p) = pane(&mut conn, d.id) else {
panic!("no pane")
};
assert_eq!(p.agent_session, "");
let a = "0f6c1c2e-8a41-4b7e-9d3a-5e2f1b7c9a10";
let b = "a1b2c3d4-0000-4000-8000-123456789abc";
assert!(set_agent_session(&conn, &p.id, a).unwrap());
assert!(
!set_agent_session(&conn, &p.id, a).unwrap(),
"same id again"
);
assert_eq!(
super::pane(&conn, &p.id)
.unwrap()
.unwrap()
.pane
.agent_session,
a
);
assert!(set_agent_session(&conn, &p.id, b).unwrap());
assert_eq!(get(&conn, d.id).unwrap().unwrap().panes[0].agent_session, b);
for bad in [
"",
"; rm -rf ~",
"A1B2C3D4-0000-4000-8000-123456789ABC",
"a1b2c3d4-0000-4000-8000-123456789abc ",
"a1b2c3d4-0000-4000-8000-123456789ab$",
"a1b2c3d400004000800-0123456789abcde",
] {
assert!(!valid_session(bad), "{bad:?}");
assert!(!set_agent_session(&conn, &p.id, bad).unwrap());
}
assert!(!set_agent_session(&conn, "nope", a).unwrap());
let tx = conn.transaction().unwrap();
assert!(close_pane(&tx, &p.id, 1).unwrap().is_some());
tx.commit().unwrap();
assert!(super::pane(&conn, &p.id).unwrap().is_none());
}
#[test]
fn resume_marks_are_set_for_known_conversations_and_taken_once() {
let mut conn = db();
let d = create(&conn, "/p", None, 0).unwrap();
let Opened::Pane(talked) = pane(&mut conn, d.id) else {
panic!("no pane")
};
let Opened::Pane(silent) = pane(&mut conn, d.id) else {
panic!("no pane")
};
let Opened::Pane(other) = pane(&mut conn, d.id) else {
panic!("no pane")
};
let a = "0f6c1c2e-8a41-4b7e-9d3a-5e2f1b7c9a10";
assert!(set_agent_session(&conn, &talked.id, a).unwrap());
assert!(set_agent_session(&conn, &other.id, a).unwrap());
let none = (Vec::<String>::new(), Vec::<String>::new());
assert_eq!(take_resume(&conn).unwrap(), none);
assert_eq!(
mark_resume(&conn, &[talked.id.clone(), silent.id.clone()]).unwrap(),
1
);
assert_eq!(take_resume(&conn).unwrap().0, vec![talked.id.clone()]);
assert_eq!(take_resume(&conn).unwrap(), none, "taken once");
mark_resume(&conn, std::slice::from_ref(&talked.id)).unwrap();
mark_resume(&conn, std::slice::from_ref(&other.id)).unwrap();
assert_eq!(take_resume(&conn).unwrap().0, vec![other.id.clone()]);
mark_resume(&conn, std::slice::from_ref(&talked.id)).unwrap();
assert_eq!(
mark_offer(
&conn,
&[talked.id.clone(), other.id.clone(), silent.id.clone()]
)
.unwrap(),
1
);
assert_eq!(
take_resume(&conn).unwrap(),
(vec![talked.id.clone()], vec![other.id.clone()])
);
}
#[test]
fn a_pane_keeps_the_folder_its_shell_moved_to() {
let mut conn = db();
let d = create(&conn, "/p", None, 0).unwrap();
let Opened::Pane(p) = pane(&mut conn, d.id) else {
panic!()
};
assert!(set_cwd(&conn, &p.id, "/p/sub").unwrap());
assert!(!set_cwd(&conn, &p.id, "/p/sub").unwrap());
assert_eq!(
super::pane(&conn, &p.id).unwrap().unwrap().pane.cwd,
"/p/sub"
);
assert!(!set_cwd(&conn, "nope", "/x").unwrap());
}
#[test]
fn a_desk_is_named_after_its_folder_and_the_second_one_is_numbered() {
let mut conn = db();
let a = create(&conn, "/home/p/snyvi", None, 0).unwrap();
let b = create(&conn, "/home/p/snyvi", None, 0).unwrap();
let c = create(&conn, "/home/p/snyvi", None, 0).unwrap();
assert_eq!(
(a.name.as_str(), b.name.as_str(), c.name.as_str()),
("snyvi", "snyvi 2", "snyvi 3")
);
assert_ne!(a.id, b.id);
assert_eq!(b.root, "/home/p/snyvi", "same folder, separate instance");
let d = create(&conn, "/home/p/snyvi", Some("chores"), 0).unwrap();
assert_eq!(d.name, "chores");
assert!(close(&mut conn, b.id, 1).unwrap().is_some());
assert_eq!(
create(&conn, "/home/p/snyvi", None, 0).unwrap().name,
"snyvi 2"
);
}
#[test]
fn panes_fill_the_lowest_free_slot_and_the_fifth_is_refused() {
let mut conn = db();
let d = create(&conn, "/p", None, 0).unwrap().id;
for _ in 0..PER_DESK {
assert!(matches!(pane(&mut conn, d), Opened::Pane(_)));
}
assert_eq!(slots(&conn, d), vec![1, 2, 3, 4]);
assert!(matches!(pane(&mut conn, d), Opened::DeskFull));
let first = get(&conn, d).unwrap().unwrap().panes[0].id.clone();
let tx = conn.transaction().unwrap();
assert_eq!(close_pane(&tx, &first, 1).unwrap(), Some((d, 1)));
tx.commit().unwrap();
assert_eq!(slots(&conn, d), vec![1, 2, 3]);
assert!(matches!(pane(&mut conn, d), Opened::Pane(p) if p.slot == 4));
assert_eq!(slots(&conn, d), vec![1, 2, 3, 4]);
}
#[test]
fn a_pane_moves_to_another_slot_and_the_one_there_takes_its_place() {
let mut conn = db();
let d = create(&conn, "/p", None, 0).unwrap().id;
let other = create(&conn, "/q", None, 0).unwrap().id;
for _ in 0..3 {
pane(&mut conn, d);
}
pane(&mut conn, other);
let ids = |conn: &Connection, d| -> Vec<String> {
get(conn, d)
.unwrap()
.unwrap()
.panes
.iter()
.map(|p| p.id.clone())
.collect()
};
let before = ids(&conn, d);
assert!(layout(&conn, d, 0.5, 0.5, Some(1)).unwrap());
assert!(layout(&conn, other, 0.5, 0.5, Some(1)).unwrap());
let tx = conn.transaction().unwrap();
assert!(move_pane(&tx, d, 1, 3).unwrap());
tx.commit().unwrap();
assert_eq!(
ids(&conn, d),
[before[2].clone(), before[1].clone(), before[0].clone()]
);
assert_eq!(
get(&conn, d).unwrap().unwrap().full_slot,
3,
"full view went with its pane"
);
assert_eq!(get(&conn, other).unwrap().unwrap().full_slot, 1);
let tx = conn.transaction().unwrap();
assert!(move_pane(&tx, d, 2, 4).unwrap());
tx.commit().unwrap();
assert_eq!(slots(&conn, d), vec![1, 3, 4]);
let tx = conn.transaction().unwrap();
assert!(!move_pane(&tx, d, 2, 1).unwrap());
assert!(!move_pane(&tx, d, 1, 5).unwrap());
assert!(!move_pane(&tx, d, 0, 1).unwrap());
tx.commit().unwrap();
assert_eq!(slots(&conn, d), vec![1, 3, 4]);
assert_eq!(slots(&conn, other), vec![1], "the other desk is untouched");
}
#[test]
fn twelve_panes_on_three_desks_all_open() {
let mut conn = db();
let desks: Vec<i64> = (0..3)
.map(|_| create(&conn, "/p", None, 0).unwrap().id)
.collect();
for d in &desks {
for _ in 0..PER_DESK {
assert!(matches!(pane(&mut conn, *d), Opened::Pane(_)));
}
assert!(matches!(pane(&mut conn, *d), Opened::DeskFull));
}
assert_eq!(panes_open(&conn).unwrap(), 3 * PER_DESK);
assert_eq!(
close(&mut conn, desks[0], 1).unwrap().unwrap().len(),
PER_DESK as usize
);
assert_eq!(
panes_open(&conn).unwrap(),
2 * PER_DESK,
"its panes closed with it"
);
}
#[test]
fn a_pane_id_is_random_hex_and_a_desk_id_is_not() {
let mut conn = db();
let d = create(&conn, "/p", None, 0).unwrap();
assert_eq!(d.id, 1, "a desk is an integer, like a project");
let Opened::Pane(a) = pane(&mut conn, d.id) else {
panic!("a pane on an empty desk")
};
let Opened::Pane(b) = pane(&mut conn, d.id) else {
panic!("a second pane")
};
assert_eq!(a.id.len(), 32);
assert!(a.id.chars().all(|c| c.is_ascii_hexdigit()));
assert_ne!(a.id, b.id);
}
#[test]
fn divider_fractions_are_clamped_and_a_missing_desk_says_so() {
let conn = db();
let d = create(&conn, "/p", None, 0).unwrap().id;
assert!(layout(&conn, d, 0.0, 2.5, None).unwrap());
let after = get(&conn, d).unwrap().unwrap();
assert_eq!((after.col, after.row), (MIN_FRACTION, MAX_FRACTION));
assert!(layout(&conn, d, f64::NAN, 0.4, None).unwrap());
assert_eq!(get(&conn, d).unwrap().unwrap().col, 0.5);
assert!(!layout(&conn, d + 99, 0.5, 0.5, None).unwrap());
assert!(layout(&conn, d, 0.5, 0.5, Some(3)).unwrap());
assert!(layout(&conn, d, 0.5, 0.5, None).unwrap());
assert_eq!(get(&conn, d).unwrap().unwrap().full_slot, 3);
assert!(layout(&conn, d, 0.5, 0.5, Some(9)).unwrap());
assert_eq!(get(&conn, d).unwrap().unwrap().full_slot, 0);
assert!(!rename(&conn, d, " ").unwrap(), "a name is not whitespace");
assert!(rename(&conn, d, "chores").unwrap());
assert_eq!(get(&conn, d).unwrap().unwrap().name, "chores");
}
#[test]
fn a_pane_on_no_desk_is_refused_without_inventing_one() {
let mut conn = db();
assert!(matches!(pane(&mut conn, 7), Opened::NoSuchDesk));
assert_eq!(panes_open(&conn).unwrap(), 0);
assert!(list(&conn).unwrap().is_empty());
}
#[test]
fn the_list_carries_empty_desks_and_their_panes_in_slot_order() {
let mut conn = db();
let a = create(&conn, "/p/one", None, 0).unwrap().id;
let b = create(&conn, "/p/two", None, 0).unwrap().id;
let _ = pane(&mut conn, b);
let _ = pane(&mut conn, b);
let listed = list(&conn).unwrap();
assert_eq!(listed.len(), 2);
assert_eq!(listed[0].id, a);
assert!(listed[0].panes.is_empty());
assert_eq!(
listed[1].panes.iter().map(|p| p.slot).collect::<Vec<_>>(),
vec![1, 2]
);
assert_eq!(listed[1].panes[0].cwd, "/p");
}
#[test]
fn a_closed_pane_leaves_no_gap_and_comes_back_at_the_end() {
let mut conn = db();
let d = create(&conn, "/p", None, 0).unwrap().id;
for _ in 0..3 {
pane(&mut conn, d);
}
let ids: Vec<String> = get(&conn, d)
.unwrap()
.unwrap()
.panes
.iter()
.map(|p| p.id.clone())
.collect();
assert!(rename_pane(&conn, &ids[1], " the tests \n").unwrap());
assert!(layout(&conn, d, 0.5, 0.5, Some(3)).unwrap());
let tx = conn.transaction().unwrap();
assert_eq!(close_pane(&tx, &ids[1], 5).unwrap(), Some((d, 2)));
assert_eq!(close_pane(&tx, "nope", 5).unwrap(), None);
tx.commit().unwrap();
let after = get(&conn, d).unwrap().unwrap();
assert_eq!(
after
.panes
.iter()
.map(|p| (p.id.clone(), p.slot))
.collect::<Vec<_>>(),
[(ids[0].clone(), 1), (ids[2].clone(), 2)]
);
assert_eq!(after.full_slot, 2, "full view follows the pane it was on");
let Restored::Pane(p) = restore_pane(&mut conn, &ids[1]).unwrap() else {
panic!("not restored")
};
assert_eq!((p.slot, p.name.as_str()), (3, "the tests"));
assert!(
matches!(restore_pane(&mut conn, &ids[1]).unwrap(), Restored::Gone),
"twice is not twice"
);
let tx = conn.transaction().unwrap();
close_pane(&tx, &ids[2], 6).unwrap();
tx.commit().unwrap();
assert_eq!(get(&conn, d).unwrap().unwrap().full_slot, 0);
}
#[test]
fn a_closed_pane_does_not_come_back_to_a_full_desk() {
let mut conn = db();
let d = create(&conn, "/p", None, 0).unwrap().id;
let Opened::Pane(first) = pane(&mut conn, d) else {
panic!()
};
let tx = conn.transaction().unwrap();
close_pane(&tx, &first.id, 1).unwrap();
tx.commit().unwrap();
for _ in 0..PER_DESK {
pane(&mut conn, d);
}
assert!(matches!(
restore_pane(&mut conn, &first.id).unwrap(),
Restored::DeskFull
));
assert_eq!(closed_on(&conn, d).unwrap(), vec![first.id.clone()]);
}
#[test]
fn closed_panes_last_until_prune_or_their_desk() {
let mut conn = db();
let d = create(&conn, "/p", None, 0).unwrap().id;
let (Opened::Pane(a), Opened::Pane(b)) = (pane(&mut conn, d), pane(&mut conn, d)) else {
panic!()
};
let tx = conn.transaction().unwrap();
close_pane(&tx, &a.id, 10).unwrap();
close_pane(&tx, &b.id, 20).unwrap();
tx.commit().unwrap();
let would = prune_closed(&conn, 15, true).unwrap();
assert_eq!(
would.iter().map(|g| g.0.clone()).collect::<Vec<_>>(),
vec![a.id.clone()]
);
assert_eq!(
closed_on(&conn, d).unwrap().len(),
2,
"a dry run removes nothing"
);
assert_eq!(prune_closed(&conn, 15, false).unwrap().len(), 1);
assert_eq!(closed_on(&conn, d).unwrap(), vec![b.id.clone()]);
close(&mut conn, d, 30).unwrap();
prune_desks(&conn, 31, false).unwrap();
let left: i64 = conn
.query_row("SELECT COUNT(*) FROM panes_closed", [], |r| r.get(0))
.unwrap();
assert_eq!(left, 0, "the desk's prune cascades");
}
#[test]
fn a_suggestion_waits_for_the_reader_and_is_theirs_once_kept() {
let mut conn = db();
let d = create(&conn, "/w", None, 0).unwrap().id;
let mine = add_note(&mut conn, d, "mine", 0).unwrap().unwrap();
let Suggested::Note(s1) =
suggest_note(&mut conn, d, " write the migration ", "claude-code", 1).unwrap()
else {
panic!()
};
assert_eq!(
(s1.text.as_str(), s1.suggested_by.as_str()),
("write the migration", "claude-code")
);
assert_eq!(
suggest_note(&mut conn, d, " ", "x", 1).unwrap(),
Suggested::Empty
);
let later = add_note(&mut conn, d, "written after", 2).unwrap().unwrap();
let order: Vec<i64> = notes(&conn, d).unwrap().iter().map(|n| n.id).collect();
assert_eq!(
order,
vec![mine.id, later.id, s1.id],
"suggestions after what is open"
);
assert!(
!tick_note(&conn, d, s1.id, &by("claude-code"), 3).unwrap(),
"not the agent's to tick"
);
for i in 0..SUGGESTIONS_PER_DESK - 1 {
assert!(matches!(
suggest_note(&mut conn, d, &format!("idea {i}"), "a", 4).unwrap(),
Suggested::Note(_)
));
}
assert_eq!(
suggest_note(&mut conn, d, "one too many", "a", 5).unwrap(),
Suggested::Full
);
assert!(keep_note(&conn, d, s1.id).unwrap());
assert!(!keep_note(&conn, d, s1.id).unwrap(), "kept once");
assert!(
!keep_note(&conn, d, mine.id).unwrap(),
"a line of the reader's is not a suggestion"
);
let kept = notes(&conn, d)
.unwrap()
.into_iter()
.find(|n| n.id == s1.id)
.unwrap();
assert!(kept.suggested_by.is_empty());
assert!(tick_note(&conn, d, s1.id, &by("claude-code"), 6).unwrap());
assert!(matches!(
suggest_note(&mut conn, d, "room again", "a", 7).unwrap(),
Suggested::Note(_)
));
assert_eq!(
suggest_note(&mut conn, 99, "no desk", "a", 7).unwrap(),
Suggested::NoSuchDesk
);
}
#[test]
fn left_off_is_one_line_and_hands_back_the_one_before() {
let conn = db();
let d = create(&conn, "/w", None, 0).unwrap().id;
assert_eq!(get(&conn, d).unwrap().unwrap().left_off, None);
let first = LeftOff {
text: "If the tests pass,\n ship the migration".into(),
at: 10,
by: "claude-code".into(),
about: "82CC8F2D3C".into(),
};
assert_eq!(set_left_off(&conn, d, &first).unwrap(), Some(None));
let got = get(&conn, d).unwrap().unwrap().left_off.unwrap();
assert_eq!(got.text, "If the tests pass, ship the migration");
assert_eq!(
(got.at, got.by.as_str(), got.about.as_str()),
(10, "claude-code", "82cc8f2d3c")
);
let long = LeftOff {
text: "x".repeat(300),
at: 11,
about: "../etc".into(),
..LeftOff::default()
};
assert_eq!(
set_left_off(&conn, d, &long).unwrap(),
Some(Some(got.clone()))
);
let now = get(&conn, d).unwrap().unwrap().left_off.unwrap();
assert_eq!(
(
now.text.chars().count(),
now.about.as_str(),
now.by.as_str()
),
(LEFT_OFF_CHARS, "", "")
);
let cleared = set_left_off(&conn, d, &LeftOff::default()).unwrap();
assert_eq!(cleared, Some(Some(now)));
assert_eq!(get(&conn, d).unwrap().unwrap().left_off, None);
assert_eq!(
set_left_off(&conn, d, &got).unwrap(),
Some(None),
"the Undo puts it back"
);
assert_eq!(get(&conn, d).unwrap().unwrap().left_off, Some(got));
assert_eq!(set_left_off(&conn, 99, &first).unwrap(), None);
}
#[test]
fn a_visit_is_written_at_most_once_a_minute() {
let mut conn = db();
let d = create(&conn, "/w", None, 0).unwrap().id;
assert!(visit(&conn, d, 1_000).unwrap());
assert!(
!visit(&conn, d, 1_030).unwrap(),
"30 s later is the same visit"
);
assert_eq!(get(&conn, d).unwrap().unwrap().visited_at, 1_000);
assert!(visit(&conn, d, 1_061).unwrap());
assert_eq!(get(&conn, d).unwrap().unwrap().visited_at, 1_061);
close(&mut conn, d, 2_000).unwrap();
assert!(!visit(&conn, d, 5_000).unwrap());
}
#[test]
fn a_desk_parks_with_its_next_step_and_comes_down() {
let conn = db();
let d = create(&conn, "/w", None, 0).unwrap().id;
assert_eq!(get(&conn, d).unwrap().unwrap().parked, None);
let to = Parked {
at: 50,
next: "Retry-After in\n whole seconds".into(),
};
assert_eq!(park(&conn, d, Some(&to)).unwrap(), Some(None));
let got = get(&conn, d).unwrap().unwrap().parked.unwrap();
assert_eq!(
(got.at, got.next.as_str()),
(50, "Retry-After in whole seconds")
);
assert_eq!(park(&conn, d, None).unwrap(), Some(Some(got)));
assert_eq!(get(&conn, d).unwrap().unwrap().parked, None);
assert_eq!(park(&conn, 99, None).unwrap(), None);
}
#[test]
fn ticks_since_leave_out_what_was_put_away_and_closed_desks() {
let mut conn = db();
let d = create(&conn, "/w", None, 0).unwrap().id;
let gone = create(&conn, "/x", None, 0).unwrap().id;
let ids: Vec<i64> = ["old", "b", "a", "put away"]
.iter()
.map(|t| add_note(&mut conn, d, t, 0).unwrap().unwrap().id)
.collect();
set_note(&conn, d, ids[0], None, Some(true), 5).unwrap();
set_note(&conn, d, ids[1], None, Some(true), 30).unwrap();
let tick = Tick {
by: "claude-code".into(),
commit: "a41c2e9".into(),
..Tick::default()
};
tick_note(&conn, d, ids[2], &tick, 20).unwrap();
set_note(&conn, d, ids[3], None, Some(true), 25).unwrap();
remove_note(&conn, d, ids[3], 26).unwrap();
let other = add_note(&mut conn, gone, "closed", 0).unwrap().unwrap().id;
set_note(&conn, gone, other, None, Some(true), 25).unwrap();
close(&mut conn, gone, 40).unwrap();
let got = done_since(&conn, 10).unwrap();
let texts: Vec<&str> = got.iter().map(|t| t.text.as_str()).collect();
assert_eq!(texts, ["a", "b"]);
assert_eq!(
(got[0].by.as_str(), got[0].commit.as_str(), got[0].at),
("claude-code", "a41c2e9", 20)
);
}
}