feat: add multi-session desktop workspace

This commit is contained in:
2026-08-13 21:34:04 +02:00
parent 12b9b83d51
commit 475f95ba70
56 changed files with 8998 additions and 3805 deletions
+858 -1
View File
@@ -1,10 +1,12 @@
use serde::Serialize;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::{json, Value};
use std::env;
use std::path::Path;
use std::sync::{Arc, Mutex};
use tauri::{AppHandle, Emitter};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::UnixStream;
use tokio::time::{sleep, Duration};
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
@@ -83,6 +85,181 @@ pub async fn request(
result_from_response(value)
}
async fn typed_request<T: DeserializeOwned>(
socket_path: &str,
operation: &str,
payload: Option<Value>,
) -> Result<T, String> {
let value = request(socket_path, operation, None, payload).await?;
serde_json::from_value(value)
.map_err(|error| format!("Bridge returned an invalid {operation} result: {error}"))
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeSummary {
pub runtime_id: String,
pub worktree_path: String,
pub state: String,
pub label: String,
pub attention: bool,
pub queue_count: u64,
pub last_activity: String,
pub opened_at: String,
pub active_tool: Option<String>,
pub agent_id: Option<String>,
pub session_id: Option<String>,
pub session_path: Option<String>,
pub error: Option<Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DirectoryWorkspace {
pub worktree_path: String,
pub is_home: bool,
pub open_count: u64,
pub working_count: u64,
pub attention_count: u64,
pub recovering_count: u64,
pub error_count: u64,
pub runtimes: Vec<RuntimeSummary>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Workspace {
pub bridge_instance_id: String,
pub latest_seq: u64,
pub directories: Vec<DirectoryWorkspace>,
pub issue: Option<Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceSummary {
pub bridge_instance_id: String,
pub latest_seq: u64,
pub open_count: u64,
pub working_count: u64,
pub attention_count: u64,
pub recovering_count: u64,
pub error_count: u64,
pub directory_count: u64,
pub resource_warning: bool,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct RuntimeResult {
pub runtime: RuntimeSummary,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CloseRuntimeResult {
pub runtime_id: String,
pub session_path: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DirectorySession {
pub path: String,
pub id: String,
pub cwd: String,
pub name: Option<String>,
pub parent_session_path: Option<String>,
pub created: Option<String>,
pub modified: String,
pub message_count: u64,
pub first_message: Option<String>,
pub is_current: bool,
pub runtime_id: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct DirectorySessionsResult {
pub sessions: Vec<DirectorySession>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeSnapshot {
pub bridge_instance_id: String,
pub latest_seq: u64,
pub runtime: RuntimeSummary,
pub state: Option<Value>,
pub stats: Option<Value>,
pub transcript: Option<Value>,
pub commands: Option<Value>,
pub models: Option<Value>,
#[serde(default)]
pub extensions: Vec<Value>,
}
pub async fn get_workspace(socket_path: &str) -> Result<Workspace, String> {
typed_request(socket_path, "get_workspace", None).await
}
pub async fn get_workspace_summary(socket_path: &str) -> Result<WorkspaceSummary, String> {
typed_request(socket_path, "get_workspace_summary", None).await
}
pub async fn create_session_runtime(
socket_path: &str,
worktree_path: &str,
) -> Result<RuntimeResult, String> {
typed_request(
socket_path,
"create_session_runtime",
Some(json!({ "worktreePath": worktree_path })),
)
.await
}
pub async fn open_session_runtime(
socket_path: &str,
worktree_path: &str,
session_path: &str,
) -> Result<RuntimeResult, String> {
typed_request(
socket_path,
"open_session_runtime",
Some(json!({ "worktreePath": worktree_path, "sessionPath": session_path })),
)
.await
}
pub async fn close_session_runtime(
socket_path: &str,
runtime_id: &str,
) -> Result<CloseRuntimeResult, String> {
typed_request(
socket_path,
"close_session_runtime",
Some(json!({ "runtimeId": runtime_id })),
)
.await
}
pub async fn list_directory_sessions(
socket_path: &str,
worktree_path: &str,
) -> Result<DirectorySessionsResult, String> {
typed_request(
socket_path,
"list_directory_sessions",
Some(json!({ "worktreePath": worktree_path })),
)
.await
}
pub async fn get_session_runtime_snapshot(
socket_path: &str,
runtime_id: &str,
) -> Result<RuntimeSnapshot, String> {
typed_request(
socket_path,
"get_session_runtime_snapshot",
Some(json!({ "runtimeId": runtime_id })),
)
.await
}
pub async fn list_agents(socket_path: &str) -> Result<Value, String> {
request(socket_path, "list_agents", None, None).await
}
@@ -246,6 +423,32 @@ pub async fn set_thinking_level(
.map(|_| ())
}
pub async fn set_session_name(socket_path: &str, agent_id: &str, name: &str) -> Result<(), String> {
request(
socket_path,
"set_session_name",
Some(agent_id),
Some(json!({ "name": name })),
)
.await
.map(|_| ())
}
pub async fn compact(
socket_path: &str,
agent_id: &str,
custom_instructions: Option<&str>,
) -> Result<(), String> {
request(
socket_path,
"compact",
Some(agent_id),
custom_instructions.map(|instructions| json!({ "customInstructions": instructions })),
)
.await
.map(|_| ())
}
pub async fn command(socket_path: &str, agent_id: &str, operation: &str) -> Result<(), String> {
request(socket_path, operation, Some(agent_id), None)
.await
@@ -268,6 +471,303 @@ pub async fn respond_to_extension(
.map(|_| ())
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceEvent {
pub bridge_instance_id: String,
pub seq: u64,
#[serde(flatten)]
pub body: serde_json::Map<String, Value>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct WorkspaceReplay {
bridge_instance_id: String,
first_available_seq: u64,
latest_seq: u64,
truncated: bool,
events: Vec<WorkspaceEvent>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(
tag = "kind",
rename_all = "camelCase",
rename_all_fields = "camelCase"
)]
pub enum WorkspaceHostEvent {
Connected {
bridge_instance_id: String,
first_available_seq: u64,
latest_seq: u64,
},
Event {
event: WorkspaceEvent,
},
ResetRequired {
bridge_instance_id: String,
latest_seq: u64,
reason: String,
},
Disconnected {
message: String,
retry_in_ms: u64,
},
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct SubscriptionCursor {
pub bridge_instance_id: Option<String>,
pub seq: u64,
}
fn accept_replay(
cursor: &mut SubscriptionCursor,
replay: &WorkspaceReplay,
) -> Vec<WorkspaceHostEvent> {
if let Some(expected) = &cursor.bridge_instance_id {
if expected != &replay.bridge_instance_id {
cursor.bridge_instance_id = Some(replay.bridge_instance_id.clone());
cursor.seq = replay.latest_seq;
return vec![WorkspaceHostEvent::ResetRequired {
bridge_instance_id: replay.bridge_instance_id.clone(),
latest_seq: replay.latest_seq,
reason: "epochChanged".into(),
}];
}
}
if replay.truncated {
cursor.bridge_instance_id = Some(replay.bridge_instance_id.clone());
cursor.seq = replay.latest_seq;
return vec![WorkspaceHostEvent::ResetRequired {
bridge_instance_id: replay.bridge_instance_id.clone(),
latest_seq: replay.latest_seq,
reason: "replayTruncated".into(),
}];
}
cursor.bridge_instance_id = Some(replay.bridge_instance_id.clone());
let mut output = vec![WorkspaceHostEvent::Connected {
bridge_instance_id: replay.bridge_instance_id.clone(),
first_available_seq: replay.first_available_seq,
latest_seq: replay.latest_seq,
}];
for event in &replay.events {
if event.bridge_instance_id != replay.bridge_instance_id || event.seq <= cursor.seq {
continue;
}
if event.seq != cursor.seq + 1 {
cursor.seq = replay.latest_seq;
output.push(WorkspaceHostEvent::ResetRequired {
bridge_instance_id: replay.bridge_instance_id.clone(),
latest_seq: replay.latest_seq,
reason: "sequenceGap".into(),
});
return output;
}
cursor.seq = event.seq;
output.push(WorkspaceHostEvent::Event {
event: event.clone(),
});
}
if cursor.seq < replay.latest_seq {
cursor.seq = replay.latest_seq;
output.push(WorkspaceHostEvent::ResetRequired {
bridge_instance_id: replay.bridge_instance_id.clone(),
latest_seq: replay.latest_seq,
reason: "sequenceGap".into(),
});
}
output
}
fn accept_live(
cursor: &mut SubscriptionCursor,
event: WorkspaceEvent,
) -> Option<WorkspaceHostEvent> {
if cursor.bridge_instance_id.as_deref() != Some(&event.bridge_instance_id) {
cursor.bridge_instance_id = Some(event.bridge_instance_id.clone());
cursor.seq = event.seq;
return Some(WorkspaceHostEvent::ResetRequired {
bridge_instance_id: event.bridge_instance_id,
latest_seq: event.seq,
reason: "epochChanged".into(),
});
}
if event.seq <= cursor.seq {
return None;
}
if event.seq != cursor.seq + 1 {
cursor.seq = event.seq;
return Some(WorkspaceHostEvent::ResetRequired {
bridge_instance_id: event.bridge_instance_id,
latest_seq: event.seq,
reason: "sequenceGap".into(),
});
}
cursor.seq = event.seq;
Some(WorkspaceHostEvent::Event { event })
}
#[derive(Debug, Default)]
pub struct SubscriptionGeneration(Mutex<u64>);
impl SubscriptionGeneration {
pub fn replace(&self) -> Result<u64, String> {
let mut current = self
.0
.lock()
.map_err(|_| "Could not replace workspace subscription".to_owned())?;
*current = current.saturating_add(1);
Ok(*current)
}
pub fn invalidate(&self) -> Result<(), String> {
self.replace().map(|_| ())
}
fn run_if_current<T>(
&self,
expected_generation: u64,
operation: impl FnOnce() -> Result<T, String>,
) -> Result<T, String> {
let current = self
.0
.lock()
.map_err(|_| "Could not inspect workspace subscription".to_owned())?;
if *current != expected_generation {
return Err("Workspace subscription was replaced".to_owned());
}
operation()
}
fn is_current(&self, expected_generation: u64) -> Result<bool, String> {
self.0
.lock()
.map(|current| *current == expected_generation)
.map_err(|_| "Could not inspect workspace subscription".to_owned())
}
}
fn retry_delay(attempt: usize) -> u64 {
match attempt {
0 => 250,
1 => 500,
2 => 1000,
_ => 2000,
}
}
fn emit_workspace(
app: &AppHandle,
generation: &SubscriptionGeneration,
expected_generation: u64,
event: WorkspaceHostEvent,
) -> Result<(), String> {
generation.run_if_current(expected_generation, || {
app.emit("workspace-bridge", event)
.map_err(|error| format!("Could not publish workspace event: {error}"))
})
}
async fn subscribe_workspace_once(
socket_path: &str,
cursor: &mut SubscriptionCursor,
app: &AppHandle,
generation: &SubscriptionGeneration,
expected_generation: u64,
) -> Result<(), String> {
let mut reader = connect_and_send(
socket_path,
"subscribe_workspace",
None,
Some(json!({ "cursor": cursor.seq })),
)
.await?;
let mut handshake = String::new();
if reader
.read_line(&mut handshake)
.await
.map_err(|error| format!("Bridge subscription failed: {error}"))?
== 0
{
return Err("Bridge subscription closed before handshake".into());
}
let replay_value: Value = serde_json::from_str(&handshake)
.map_err(|error| format!("Bridge emitted invalid JSON: {error}"))?;
let replay: WorkspaceReplay = serde_json::from_value(result_from_response(replay_value)?)
.map_err(|error| format!("Bridge returned an invalid workspace replay: {error}"))?;
for event in accept_replay(cursor, &replay) {
emit_workspace(app, generation, expected_generation, event)?;
}
loop {
let mut line = String::new();
if reader
.read_line(&mut line)
.await
.map_err(|error| format!("Bridge subscription failed: {error}"))?
== 0
{
return Err("Bridge subscription closed".into());
}
let envelope: Value = serde_json::from_str(&line)
.map_err(|error| format!("Bridge emitted invalid JSON: {error}"))?;
let event: WorkspaceEvent = serde_json::from_value(
envelope
.get("event")
.cloned()
.ok_or_else(|| "Bridge event envelope is missing event".to_owned())?,
)
.map_err(|error| format!("Bridge emitted an invalid workspace event: {error}"))?;
if let Some(event) = accept_live(cursor, event) {
emit_workspace(app, generation, expected_generation, event)?;
}
}
}
pub async fn subscribe_workspace(
socket_path: String,
mut cursor: SubscriptionCursor,
app: AppHandle,
generation: Arc<SubscriptionGeneration>,
expected_generation: u64,
) -> Result<(), String> {
let mut attempt = 0usize;
loop {
if !generation.is_current(expected_generation)? {
return Ok(());
}
match subscribe_workspace_once(
&socket_path,
&mut cursor,
&app,
&generation,
expected_generation,
)
.await
{
Ok(()) => attempt = 0,
Err(error) => {
let delay = retry_delay(attempt);
if error == "Workspace subscription was replaced" {
return Ok(());
}
emit_workspace(
&app,
&generation,
expected_generation,
WorkspaceHostEvent::Disconnected {
message: error,
retry_in_ms: delay,
},
)?;
sleep(Duration::from_millis(delay)).await;
attempt = (attempt + 1).min(3);
}
}
}
}
pub async fn subscribe(
socket_path: String,
agent_id: String,
@@ -445,4 +945,361 @@ mod tests {
server.await.expect("server succeeds");
std::fs::remove_file(path).expect("socket cleanup");
}
fn runtime(id: &str) -> Value {
json!({
"runtimeId": id,
"worktreePath": "/tmp/project",
"state": "idle",
"label": "Session",
"attention": false,
"queueCount": 0,
"lastActivity": "2026-01-01T00:00:00Z",
"openedAt": "2026-01-01T00:00:00Z"
})
}
async fn assert_typed_frame<F, Fut>(operation: &'static str, payload: Value, call: F)
where
F: FnOnce(String) -> Fut,
Fut: std::future::Future<Output = Result<(), String>>,
{
let path = format!(
"{}/pi-status-ui-frame-{operation}-{}-{}.sock",
env::temp_dir().display(),
std::process::id(),
operation.len()
);
let _ = std::fs::remove_file(&path);
let listener = UnixListener::bind(&path).expect("listener");
let expected_payload = payload.clone();
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("connection");
let mut reader = BufReader::new(stream);
let mut line = String::new();
reader.read_line(&mut line).await.unwrap();
let frame: Value = serde_json::from_str(&line).unwrap();
assert_eq!(frame["op"], operation);
assert_eq!(
frame.get("payload").cloned().unwrap_or(Value::Null),
expected_payload
);
let result = match operation {
"close_session_runtime" => json!({"runtimeId":"r"}),
"list_directory_sessions" => json!({"sessions":[]}),
"get_session_runtime_snapshot" => {
json!({"bridgeInstanceId":"b","latestSeq":0,"runtime":runtime("r"),"extensions":[]})
}
_ => json!({"runtime":runtime("r")}),
};
let response = format!("{}\n", json!({"id":"tauri-ui","ok":true,"result":result}));
reader
.get_mut()
.write_all(response.as_bytes())
.await
.unwrap();
});
call(path.clone()).await.unwrap();
server.await.unwrap();
std::fs::remove_file(path).unwrap();
}
async fn assert_no_payload<T, F, Fut>(operation: &'static str, result: Value, call: F) -> T
where
T: Send + 'static,
F: FnOnce(String) -> Fut,
Fut: std::future::Future<Output = Result<T, String>>,
{
let path = format!(
"{}/pi-status-ui-empty-{operation}-{}.sock",
env::temp_dir().display(),
std::process::id()
);
let _ = std::fs::remove_file(&path);
let listener = UnixListener::bind(&path).unwrap();
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let mut reader = BufReader::new(stream);
let mut line = String::new();
reader.read_line(&mut line).await.unwrap();
let frame: Value = serde_json::from_str(&line).unwrap();
assert_eq!(frame["op"], operation);
assert!(frame.get("payload").is_none());
let response = format!("{}\n", json!({"id":"tauri-ui","ok":true,"result":result}));
reader
.get_mut()
.write_all(response.as_bytes())
.await
.unwrap();
});
let value = call(path.clone()).await.unwrap();
server.await.unwrap();
std::fs::remove_file(path).unwrap();
value
}
#[tokio::test]
async fn workspace_wrappers_emit_exact_operations_without_payloads() {
let workspace: Workspace = assert_no_payload(
"get_workspace",
json!({"bridgeInstanceId":"b","latestSeq":0,"directories":[]}),
|path| async move { get_workspace(&path).await },
)
.await;
assert_eq!(workspace.bridge_instance_id, "b");
let summary: WorkspaceSummary = assert_no_payload(
"get_workspace_summary",
json!({"bridgeInstanceId":"b","latestSeq":0,"openCount":0,"workingCount":0,"attentionCount":0,"recoveringCount":0,"errorCount":0,"directoryCount":0,"resourceWarning":false}),
|path| async move { get_workspace_summary(&path).await },
)
.await;
assert_eq!(summary.open_count, 0);
}
#[tokio::test]
async fn runtime_wrappers_emit_exact_operations_and_payloads() {
assert_typed_frame(
"create_session_runtime",
json!({"worktreePath":"/tmp/project"}),
|path| async move {
create_session_runtime(&path, "/tmp/project")
.await
.map(|_| ())
},
)
.await;
assert_typed_frame(
"open_session_runtime",
json!({"worktreePath":"/tmp/project","sessionPath":"/tmp/project/s.jsonl"}),
|path| async move {
open_session_runtime(&path, "/tmp/project", "/tmp/project/s.jsonl")
.await
.map(|_| ())
},
)
.await;
assert_typed_frame(
"close_session_runtime",
json!({"runtimeId":"r"}),
|path| async move { close_session_runtime(&path, "r").await.map(|_| ()) },
)
.await;
assert_typed_frame(
"list_directory_sessions",
json!({"worktreePath":"/tmp/project"}),
|path| async move {
list_directory_sessions(&path, "/tmp/project")
.await
.map(|_| ())
},
)
.await;
assert_typed_frame(
"get_session_runtime_snapshot",
json!({"runtimeId":"r"}),
|path| async move { get_session_runtime_snapshot(&path, "r").await.map(|_| ()) },
)
.await;
}
#[test]
fn decodes_multi_runtime_workspace_dormant_snapshot_and_directory_sessions() {
let workspace: Workspace = serde_json::from_value(json!({
"bridgeInstanceId":"bridge", "latestSeq":4,
"directories":[{"worktreePath":"/tmp/project","isHome":true,"openCount":2,"workingCount":1,"attentionCount":0,"recoveringCount":0,"errorCount":0,"runtimes":[runtime("one"),runtime("two")]}]
})).unwrap();
assert_eq!(workspace.directories[0].runtimes.len(), 2);
let snapshot: RuntimeSnapshot = serde_json::from_value(json!({"bridgeInstanceId":"bridge","latestSeq":4,"runtime":runtime("failed"),"extensions":[]})).unwrap();
assert!(snapshot.state.is_none());
let sessions: DirectorySessionsResult = serde_json::from_value(json!({
"sessions": [
{"path":"/tmp/project/one.jsonl","id":"one","cwd":"/tmp/project","modified":"2026-01-01T00:00:00Z","messageCount":2,"isCurrent":false,"runtimeId":"runtime-one"},
{"path":"/tmp/project/two.jsonl","id":"two","cwd":"/tmp/project","modified":"2026-01-02T00:00:00Z","messageCount":0,"isCurrent":false}
]
})).unwrap();
assert_eq!(
sessions.sessions[0].runtime_id.as_deref(),
Some("runtime-one")
);
assert!(sessions.sessions[1].runtime_id.is_none());
}
#[tokio::test]
async fn workspace_subscription_frame_uses_global_cursor_without_agent() {
let path = format!(
"{}/pi-status-ui-workspace-subscribe-{}.sock",
env::temp_dir().display(),
std::process::id()
);
let _ = std::fs::remove_file(&path);
let listener = UnixListener::bind(&path).unwrap();
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let mut reader = BufReader::new(stream);
let mut line = String::new();
reader.read_line(&mut line).await.unwrap();
let frame: Value = serde_json::from_str(&line).unwrap();
assert_eq!(frame["op"], "subscribe_workspace");
assert_eq!(frame["payload"], json!({"cursor":17}));
assert!(frame.get("agentId").is_none());
});
let _reader = connect_and_send(
&path,
"subscribe_workspace",
None,
Some(json!({"cursor":17})),
)
.await
.unwrap();
server.await.unwrap();
std::fs::remove_file(path).unwrap();
}
fn event(epoch: &str, seq: u64) -> WorkspaceEvent {
serde_json::from_value(
json!({"bridgeInstanceId":epoch,"seq":seq,"type":"runtime_event","data":{}}),
)
.unwrap()
}
#[test]
fn workspace_host_events_use_exact_camel_case_json() {
let event = event("bridge-1", 7);
let cases = [
(
WorkspaceHostEvent::Connected {
bridge_instance_id: "bridge-1".into(),
first_available_seq: 3,
latest_seq: 7,
},
json!({
"kind": "connected",
"bridgeInstanceId": "bridge-1",
"firstAvailableSeq": 3,
"latestSeq": 7
}),
),
(
WorkspaceHostEvent::Event {
event: event.clone(),
},
json!({ "kind": "event", "event": event }),
),
(
WorkspaceHostEvent::ResetRequired {
bridge_instance_id: "bridge-1".into(),
latest_seq: 7,
reason: "sequenceGap".into(),
},
json!({
"kind": "resetRequired",
"bridgeInstanceId": "bridge-1",
"latestSeq": 7,
"reason": "sequenceGap"
}),
),
(
WorkspaceHostEvent::Disconnected {
message: "offline".into(),
retry_in_ms: 500,
},
json!({
"kind": "disconnected",
"message": "offline",
"retryInMs": 500
}),
),
];
for (host_event, expected) in cases {
assert_eq!(serde_json::to_value(host_event).unwrap(), expected);
}
}
#[test]
fn replacement_waits_for_an_in_flight_emission_and_rejects_stale_work() {
use std::sync::mpsc;
use std::thread;
use std::time::Duration as StdDuration;
let generation = Arc::new(SubscriptionGeneration::default());
let first = generation.replace().unwrap();
let (entered_tx, entered_rx) = mpsc::channel();
let (release_tx, release_rx) = mpsc::channel();
let emitter_generation = Arc::clone(&generation);
let emitter = thread::spawn(move || {
emitter_generation.run_if_current(first, || {
entered_tx.send(()).unwrap();
release_rx.recv().unwrap();
Ok(())
})
});
entered_rx.recv().unwrap();
let replacement_generation = Arc::clone(&generation);
let (replaced_tx, replaced_rx) = mpsc::channel();
let replacement = thread::spawn(move || {
let next = replacement_generation.replace().unwrap();
replaced_tx.send(next).unwrap();
});
assert!(replaced_rx
.recv_timeout(StdDuration::from_millis(50))
.is_err());
release_tx.send(()).unwrap();
assert!(emitter.join().unwrap().is_ok());
assert_eq!(replaced_rx.recv().unwrap(), first + 1);
replacement.join().unwrap();
assert_eq!(
generation.run_if_current(first, || Ok(())),
Err("Workspace subscription was replaced".to_owned())
);
assert_eq!(
(0..6).map(retry_delay).collect::<Vec<_>>(),
vec![250, 500, 1000, 2000, 2000, 2000]
);
}
#[test]
fn replay_and_live_decisions_handle_epochs_truncation_and_gaps() {
let mut cursor = SubscriptionCursor {
bridge_instance_id: Some("a".into()),
seq: 1,
};
let replay = WorkspaceReplay {
bridge_instance_id: "a".into(),
first_available_seq: 1,
latest_seq: 3,
truncated: false,
events: vec![event("a", 2), event("a", 3)],
};
assert_eq!(accept_replay(&mut cursor, &replay).len(), 3);
assert_eq!(cursor.seq, 3);
assert!(matches!(
accept_live(&mut cursor, event("a", 4)),
Some(WorkspaceHostEvent::Event { .. })
));
assert!(accept_live(&mut cursor, event("a", 4)).is_none());
assert!(
matches!(accept_live(&mut cursor, event("a",6)), Some(WorkspaceHostEvent::ResetRequired { reason, .. }) if reason == "sequenceGap")
);
let changed = WorkspaceReplay {
bridge_instance_id: "b".into(),
first_available_seq: 1,
latest_seq: 9,
truncated: false,
events: vec![],
};
assert!(
matches!(&accept_replay(&mut cursor, &changed)[0], WorkspaceHostEvent::ResetRequired { reason, .. } if reason == "epochChanged")
);
let truncated = WorkspaceReplay {
bridge_instance_id: "b".into(),
first_available_seq: 10,
latest_seq: 12,
truncated: true,
events: vec![],
};
assert!(
matches!(&accept_replay(&mut cursor, &truncated)[0], WorkspaceHostEvent::ResetRequired { reason, .. } if reason == "replayTruncated")
);
}
}
+176 -5
View File
@@ -1,10 +1,29 @@
mod bridge;
mod ui_state;
use serde_json::Value;
use std::sync::Mutex;
use std::sync::{Arc, Mutex};
use tauri::{async_runtime::JoinHandle, AppHandle, Emitter, Manager, State};
struct Subscription(Mutex<Option<JoinHandle<()>>>);
struct LegacySubscription(Mutex<Option<JoinHandle<()>>>);
struct WorkspaceSubscription {
generation: Arc<bridge::SubscriptionGeneration>,
task: Mutex<Option<JoinHandle<()>>>,
}
fn replace_subscription<T>(
task_slot: &Mutex<Option<T>>,
generation: &bridge::SubscriptionGeneration,
create: impl FnOnce(u64) -> T,
) -> Result<Option<T>, String> {
let mut slot = task_slot
.lock()
.map_err(|_| "Could not update workspace subscription".to_owned())?;
let expected_generation = generation.replace()?;
let previous = slot.take();
*slot = Some(create(expected_generation));
Ok(previous)
}
#[derive(Debug, PartialEq, Eq)]
enum WindowAction {
@@ -26,6 +45,66 @@ fn socket_path() -> Result<String, String> {
bridge::default_socket_path()
}
#[tauri::command]
async fn get_workspace() -> Result<bridge::Workspace, String> {
bridge::get_workspace(&socket_path()?).await
}
#[tauri::command]
async fn get_workspace_summary() -> Result<bridge::WorkspaceSummary, String> {
bridge::get_workspace_summary(&socket_path()?).await
}
#[tauri::command]
async fn create_session_runtime(worktree_path: String) -> Result<bridge::RuntimeResult, String> {
bridge::create_session_runtime(&socket_path()?, &worktree_path).await
}
#[tauri::command]
async fn open_session_runtime(
worktree_path: String,
session_path: String,
) -> Result<bridge::RuntimeResult, String> {
bridge::open_session_runtime(&socket_path()?, &worktree_path, &session_path).await
}
#[tauri::command]
async fn close_session_runtime(runtime_id: String) -> Result<bridge::CloseRuntimeResult, String> {
bridge::close_session_runtime(&socket_path()?, &runtime_id).await
}
#[tauri::command]
async fn list_directory_sessions(
worktree_path: String,
) -> Result<bridge::DirectorySessionsResult, String> {
bridge::list_directory_sessions(&socket_path()?, &worktree_path).await
}
#[tauri::command]
async fn get_session_runtime_snapshot(
runtime_id: String,
) -> Result<bridge::RuntimeSnapshot, String> {
bridge::get_session_runtime_snapshot(&socket_path()?, &runtime_id).await
}
#[tauri::command]
fn load_ui_state(app: AppHandle) -> Result<ui_state::UiStateV1, String> {
let directory = app
.path()
.app_data_dir()
.map_err(|error| error.to_string())?;
ui_state::load_from(&directory)
}
#[tauri::command]
fn save_ui_state(app: AppHandle, state: ui_state::UiStateV1) -> Result<(), String> {
let directory = app
.path()
.app_data_dir()
.map_err(|error| error.to_string())?;
ui_state::save_to(&directory, &state)
}
#[tauri::command]
async fn list_agents() -> Result<Value, String> {
bridge::list_agents(&socket_path()?).await
@@ -96,6 +175,16 @@ async fn set_thinking_level(agent_id: String, level: String) -> Result<(), Strin
bridge::set_thinking_level(&socket_path()?, &agent_id, &level).await
}
#[tauri::command]
async fn set_session_name(agent_id: String, name: String) -> Result<(), String> {
bridge::set_session_name(&socket_path()?, &agent_id, &name).await
}
#[tauri::command]
async fn compact(agent_id: String, custom_instructions: Option<String>) -> Result<(), String> {
bridge::compact(&socket_path()?, &agent_id, custom_instructions.as_deref()).await
}
#[tauri::command]
async fn respond_to_extension(
agent_id: String,
@@ -108,7 +197,7 @@ async fn respond_to_extension(
#[tauri::command]
fn subscribe_agent(
app: AppHandle,
subscriptions: State<'_, Subscription>,
subscriptions: State<'_, LegacySubscription>,
agent_id: String,
cursor: u64,
) -> Result<(), String> {
@@ -140,11 +229,65 @@ fn subscribe_agent(
Ok(())
}
#[tauri::command]
fn subscribe_workspace(
app: AppHandle,
subscription: State<'_, WorkspaceSubscription>,
cursor: Option<ui_state::WorkspaceCursor>,
) -> Result<(), String> {
let socket = socket_path()?;
let event_app = app.clone();
let generation = Arc::clone(&subscription.generation);
let previous = replace_subscription(
&subscription.task,
&subscription.generation,
move |expected_generation| {
tauri::async_runtime::spawn(async move {
let initial = bridge::SubscriptionCursor {
bridge_instance_id: cursor
.as_ref()
.map(|value| value.bridge_instance_id.clone()),
seq: cursor.map(|value| value.seq).unwrap_or(0),
};
let _ = bridge::subscribe_workspace(
socket,
initial,
event_app,
generation,
expected_generation,
)
.await;
})
},
)?;
if let Some(task) = previous {
task.abort();
}
Ok(())
}
#[tauri::command]
fn unsubscribe_workspace(subscription: State<'_, WorkspaceSubscription>) -> Result<(), String> {
let mut slot = subscription
.task
.lock()
.map_err(|_| "Could not clear workspace subscription".to_owned())?;
subscription.generation.invalidate()?;
if let Some(task) = slot.take() {
task.abort();
}
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let builder = tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.manage(Subscription(Mutex::new(None)))
.manage(LegacySubscription(Mutex::new(None)))
.manage(WorkspaceSubscription {
generation: Arc::new(bridge::SubscriptionGeneration::default()),
task: Mutex::new(None),
})
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
if let Some(window) = app.get_webview_window("main") {
match window_action(&args, window.is_visible().unwrap_or(false)) {
@@ -159,6 +302,17 @@ pub fn run() {
}
}))
.invoke_handler(tauri::generate_handler![
get_workspace,
get_workspace_summary,
create_session_runtime,
open_session_runtime,
close_session_runtime,
list_directory_sessions,
get_session_runtime_snapshot,
load_ui_state,
save_ui_state,
subscribe_workspace,
unsubscribe_workspace,
list_agents,
list_directories,
select_worktree,
@@ -173,6 +327,8 @@ pub fn run() {
restart,
set_model,
set_thinking_level,
set_session_name,
compact,
respond_to_extension,
subscribe_agent
]);
@@ -186,6 +342,21 @@ pub fn run() {
mod tests {
use super::*;
#[test]
fn serialized_subscription_replacement_returns_the_previous_task() {
let tasks = Mutex::new(None::<u64>);
let generation = bridge::SubscriptionGeneration::default();
assert_eq!(
replace_subscription(&tasks, &generation, |current| current).unwrap(),
None
);
assert_eq!(
replace_subscription(&tasks, &generation, |current| current).unwrap(),
Some(1)
);
assert_eq!(*tasks.lock().unwrap(), Some(2));
}
#[test]
fn toggle_hides_a_visible_window_and_shows_a_hidden_window() {
let toggle = vec!["--toggle".to_owned()];
@@ -193,7 +364,7 @@ mod tests {
assert_eq!(window_action(&toggle, true), WindowAction::Hide);
assert_eq!(window_action(&toggle, false), WindowAction::ShowAndFocus);
assert_eq!(
window_action(&vec!["--show".to_owned()], true),
window_action(&["--show".to_owned()], true),
WindowAction::ShowAndFocus
);
}
+440
View File
@@ -0,0 +1,440 @@
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fs::{self, OpenOptions};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
const VERSION: u32 = 1;
const MAX_ITEMS: usize = 256;
const MAX_TOTAL_ITEMS: usize = 2048;
const MAX_DRAFT_BYTES: usize = 128 * 1024;
const MAX_TOTAL_DRAFT_BYTES: usize = 1024 * 1024;
const MAX_TOTAL_STRING_BYTES: usize = 1536 * 1024;
const MAX_FILE_BYTES: usize = 2 * 1024 * 1024;
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct WorkspaceCursor {
pub bridge_instance_id: String,
pub seq: u64,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ScrollState {
pub at_bottom: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub scroll_top: Option<f64>,
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct LastSeenState {
pub bridge_instance_id: String,
pub seq: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_activity: Option<String>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct UiStateV1 {
pub version: u32,
#[serde(default)]
pub directory_order: Vec<String>,
#[serde(default)]
pub runtime_order_by_directory: BTreeMap<String, Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub selected_runtime_id: Option<String>,
#[serde(default)]
pub drafts: BTreeMap<String, String>,
#[serde(default)]
pub scroll: BTreeMap<String, ScrollState>,
#[serde(default)]
pub last_seen: BTreeMap<String, LastSeenState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workspace_cursor: Option<WorkspaceCursor>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub interface_scale: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dismissed_collision_warning: Option<bool>,
}
impl Default for UiStateV1 {
fn default() -> Self {
Self {
version: VERSION,
directory_order: Vec::new(),
runtime_order_by_directory: BTreeMap::new(),
selected_runtime_id: None,
drafts: BTreeMap::new(),
scroll: BTreeMap::new(),
last_seen: BTreeMap::new(),
workspace_cursor: None,
interface_scale: None,
dismissed_collision_warning: None,
}
}
}
fn bounded(value: &str, field: &str) -> Result<(), String> {
if value.is_empty() || value.len() > 4096 {
Err(format!("{field} must contain 1 to 4096 bytes"))
} else {
Ok(())
}
}
pub fn validate(state: &UiStateV1) -> Result<(), String> {
if state.version != VERSION {
return Err(format!("Unsupported UI state version: {}", state.version));
}
if state.directory_order.len() > MAX_ITEMS
|| state.runtime_order_by_directory.len() > MAX_ITEMS
|| state.drafts.len() > MAX_ITEMS
|| state.scroll.len() > MAX_ITEMS
|| state.last_seen.len() > MAX_ITEMS
{
return Err("UI state contains too many entries".to_owned());
}
let mut total_items = state.directory_order.len()
+ state.runtime_order_by_directory.len()
+ state.drafts.len()
+ state.scroll.len()
+ state.last_seen.len();
let mut total_string_bytes = 0usize;
for path in &state.directory_order {
bounded(path, "directory path")?;
total_string_bytes = total_string_bytes.saturating_add(path.len());
}
for (path, runtimes) in &state.runtime_order_by_directory {
bounded(path, "directory path")?;
total_string_bytes = total_string_bytes.saturating_add(path.len());
if runtimes.len() > MAX_ITEMS {
return Err("UI state contains too many runtime entries".to_owned());
}
total_items = total_items.saturating_add(runtimes.len());
for runtime_id in runtimes {
bounded(runtime_id, "runtime id")?;
total_string_bytes = total_string_bytes.saturating_add(runtime_id.len());
}
}
if total_items > MAX_TOTAL_ITEMS {
return Err("UI state contains too many aggregate entries".to_owned());
}
if let Some(runtime_id) = &state.selected_runtime_id {
bounded(runtime_id, "selected runtime id")?;
total_string_bytes = total_string_bytes.saturating_add(runtime_id.len());
}
let mut total_draft_bytes = 0usize;
for (runtime_id, draft) in &state.drafts {
bounded(runtime_id, "draft runtime id")?;
if draft.len() > MAX_DRAFT_BYTES {
return Err("A UI draft exceeds the size limit".to_owned());
}
total_draft_bytes = total_draft_bytes.saturating_add(draft.len());
total_string_bytes = total_string_bytes
.saturating_add(runtime_id.len())
.saturating_add(draft.len());
}
if total_draft_bytes > MAX_TOTAL_DRAFT_BYTES {
return Err("UI drafts exceed the total size limit".to_owned());
}
for (runtime_id, scroll) in &state.scroll {
bounded(runtime_id, "runtime id")?;
total_string_bytes = total_string_bytes.saturating_add(runtime_id.len());
if scroll
.scroll_top
.is_some_and(|value| !value.is_finite() || value < 0.0)
{
return Err("Scroll position is invalid".to_owned());
}
}
for (runtime_id, last_seen) in &state.last_seen {
bounded(runtime_id, "runtime id")?;
bounded(
&last_seen.bridge_instance_id,
"last-seen bridge instance id",
)?;
total_string_bytes = total_string_bytes
.saturating_add(runtime_id.len())
.saturating_add(last_seen.bridge_instance_id.len());
if let Some(activity) = &last_seen.last_activity {
bounded(activity, "last activity")?;
total_string_bytes = total_string_bytes.saturating_add(activity.len());
}
}
if let Some(cursor) = &state.workspace_cursor {
bounded(&cursor.bridge_instance_id, "bridge instance id")?;
total_string_bytes = total_string_bytes.saturating_add(cursor.bridge_instance_id.len());
}
if total_string_bytes > MAX_TOTAL_STRING_BYTES {
return Err("UI state strings exceed the total size limit".to_owned());
}
if let Some(scale) = state.interface_scale {
if !scale.is_finite() || !(0.5..=2.0).contains(&scale) {
return Err("Interface scale is outside the supported range".to_owned());
}
}
Ok(())
}
pub fn state_path(app_data_dir: &Path) -> PathBuf {
app_data_dir.join("ui-workspace-v1.json")
}
pub fn load_from(app_data_dir: &Path) -> Result<UiStateV1, String> {
let path = state_path(app_data_dir);
let mut file = match fs::File::open(&path) {
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return Ok(UiStateV1::default())
}
Err(error) => return Err(format!("Could not read UI state: {error}")),
};
let metadata = file
.metadata()
.map_err(|error| format!("Could not inspect UI state: {error}"))?;
if metadata.len() > MAX_FILE_BYTES as u64 {
return Err("UI state file exceeds the size limit".to_owned());
}
let mut content = Vec::with_capacity(metadata.len() as usize);
Read::by_ref(&mut file)
.take((MAX_FILE_BYTES + 1) as u64)
.read_to_end(&mut content)
.map_err(|error| format!("Could not read UI state: {error}"))?;
if content.len() > MAX_FILE_BYTES {
return Err("UI state file exceeds the size limit".to_owned());
}
let state: UiStateV1 = serde_json::from_slice(&content)
.map_err(|error| format!("UI state is corrupt or unsupported: {error}"))?;
validate(&state)?;
Ok(state)
}
pub fn save_to(app_data_dir: &Path, state: &UiStateV1) -> Result<(), String> {
validate(state)?;
fs::create_dir_all(app_data_dir)
.map_err(|error| format!("Could not create app data: {error}"))?;
set_mode(app_data_dir, 0o700)?;
let target = state_path(app_data_dir);
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let temporary = app_data_dir.join(format!(".ui-workspace-{}-{nonce}.tmp", std::process::id()));
let encoded = serde_json::to_vec_pretty(state).map_err(|error| error.to_string())?;
if encoded.len().saturating_add(1) > MAX_FILE_BYTES {
return Err("UI state file exceeds the size limit".to_owned());
}
let result = (|| {
let mut file = create_private_file(&temporary)?;
file.write_all(&encoded)
.map_err(|error| format!("Could not write UI state: {error}"))?;
file.write_all(b"\n")
.map_err(|error| format!("Could not write UI state: {error}"))?;
file.sync_all()
.map_err(|error| format!("Could not sync UI state: {error}"))?;
fs::rename(&temporary, &target)
.map_err(|error| format!("Could not replace UI state: {error}"))?;
set_mode(&target, 0o600)?;
sync_directory(app_data_dir)?;
Ok(())
})();
if result.is_err() {
let _ = fs::remove_file(&temporary);
}
result
}
#[cfg(unix)]
fn sync_directory(path: &Path) -> Result<(), String> {
let directory = OpenOptions::new()
.read(true)
.open(path)
.map_err(|error| format!("Could not open app data directory for sync: {error}"))?;
directory
.sync_all()
.map_err(|error| format!("Could not sync app data directory: {error}"))
}
#[cfg(not(unix))]
fn sync_directory(_path: &Path) -> Result<(), String> {
Ok(())
}
#[cfg(unix)]
fn create_private_file(path: &Path) -> Result<std::fs::File, String> {
use std::os::unix::fs::OpenOptionsExt;
OpenOptions::new()
.create_new(true)
.write(true)
.mode(0o600)
.open(path)
.map_err(|error| format!("Could not create temporary UI state: {error}"))
}
#[cfg(not(unix))]
fn create_private_file(path: &Path) -> Result<std::fs::File, String> {
OpenOptions::new()
.create_new(true)
.write(true)
.open(path)
.map_err(|error| format!("Could not create temporary UI state: {error}"))
}
#[cfg(unix)]
fn set_mode(path: &Path, mode: u32) -> Result<(), String> {
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(mode))
.map_err(|error| format!("Could not secure UI state path: {error}"))
}
#[cfg(not(unix))]
fn set_mode(_path: &Path, _mode: u32) -> Result<(), String> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn temp_dir(name: &str) -> PathBuf {
std::env::temp_dir().join(format!(
"pi-ui-state-{name}-{}-{}",
std::process::id(),
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
))
}
#[test]
fn owner_only_round_trip_and_missing_default() {
let root = temp_dir("roundtrip");
assert_eq!(load_from(&root).unwrap(), UiStateV1::default());
let mut state = UiStateV1 {
selected_runtime_id: Some("runtime-1".into()),
interface_scale: Some(1.1),
..UiStateV1::default()
};
state.drafts.insert("runtime-1".into(), "draft".into());
save_to(&root, &state).unwrap();
assert_eq!(load_from(&root).unwrap(), state);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
assert_eq!(
fs::metadata(&root).unwrap().permissions().mode() & 0o777,
0o700
);
assert_eq!(
fs::metadata(state_path(&root))
.unwrap()
.permissions()
.mode()
& 0o777,
0o600
);
}
fs::remove_dir_all(root).unwrap();
}
fn temporary_files(root: &Path) -> Vec<PathBuf> {
fs::read_dir(root)
.unwrap()
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| {
name.starts_with(".ui-workspace-") && name.ends_with(".tmp")
})
})
.collect()
}
#[test]
fn second_save_replaces_valid_state_without_leaving_temporary_files() {
let root = temp_dir("replace");
let first = UiStateV1 {
selected_runtime_id: Some("runtime-1".into()),
..UiStateV1::default()
};
let second = UiStateV1 {
selected_runtime_id: Some("runtime-2".into()),
interface_scale: Some(1.2),
..UiStateV1::default()
};
save_to(&root, &first).unwrap();
save_to(&root, &second).unwrap();
assert_eq!(load_from(&root).unwrap(), second);
assert!(temporary_files(&root).is_empty());
fs::remove_dir_all(root).unwrap();
}
#[test]
fn rejects_corruption_versions_unknown_fields_and_limits_without_overwrite() {
let root = temp_dir("invalid");
fs::create_dir_all(&root).unwrap();
let path = state_path(&root);
for invalid in [
"{broken",
r#"{"version":2}"#,
r#"{"version":1,"transcript":[]}"#,
] {
fs::write(&path, invalid).unwrap();
assert!(load_from(&root).is_err());
assert_eq!(fs::read_to_string(&path).unwrap(), invalid);
}
let baseline = UiStateV1 {
selected_runtime_id: Some("baseline".into()),
..UiStateV1::default()
};
save_to(&root, &baseline).unwrap();
let original = fs::read(&path).unwrap();
let mut oversized = UiStateV1::default();
oversized
.drafts
.insert("runtime".into(), "x".repeat(MAX_DRAFT_BYTES + 1));
assert!(save_to(&root, &oversized).is_err());
assert_eq!(fs::read(&path).unwrap(), original);
assert!(temporary_files(&root).is_empty());
fs::remove_dir_all(root).unwrap();
}
#[test]
fn rejects_aggregate_and_raw_file_size_limits() {
let root = temp_dir("aggregate");
fs::create_dir_all(&root).unwrap();
let mut aggregate = UiStateV1::default();
for directory in 0..9 {
aggregate.runtime_order_by_directory.insert(
format!("/directory-{directory}"),
(0..MAX_ITEMS)
.map(|runtime| format!("runtime-{directory}-{runtime}"))
.collect(),
);
}
assert!(validate(&aggregate)
.unwrap_err()
.contains("aggregate entries"));
let path = state_path(&root);
fs::write(&path, vec![b' '; MAX_FILE_BYTES + 1]).unwrap();
assert!(load_from(&root)
.unwrap_err()
.contains("file exceeds the size limit"));
assert_eq!(
fs::metadata(&path).unwrap().len(),
(MAX_FILE_BYTES + 1) as u64
);
assert!(temporary_files(&root).is_empty());
fs::remove_dir_all(root).unwrap();
}
}