From 39d521231b0e8fcf56db2f3bbbb363e446840cd4 Mon Sep 17 00:00:00 2001 From: Nate Date: Fri, 20 Feb 2026 21:44:16 -0800 Subject: [PATCH] feat: display session URL on SessionStart hook Generate a nanoid share slug during the SessionStart hook and print the threader.sh URL to stderr so Claude Code shows it to the user. The slug is stored locally and sent with the session create request so the backend uses the same slug. Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 10 ++++++++++ Cargo.toml | 1 + src/cli/hook.rs | 10 ++++++++++ src/sync/uploader.rs | 11 +++++++++++ 4 files changed, 32 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index e35478d..6d1c1a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1430,6 +1430,15 @@ dependencies = [ "pxfm", ] +[[package]] +name = "nanoid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +dependencies = [ + "rand 0.8.5", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -2699,6 +2708,7 @@ dependencies = [ "ed25519-dalek", "image", "libc", + "nanoid", "open", "reqwest", "semver", diff --git a/Cargo.toml b/Cargo.toml index f61345b..946a0b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ url = "2" image = "0.25" sentry = { version = "0.35", default-features = false, features = ["backtrace", "contexts", "tracing", "reqwest", "rustls", "panic"] } uuid = { version = "1", features = ["v4"] } +nanoid = "0.4" [dev-dependencies] tempfile = "3" diff --git a/src/cli/hook.rs b/src/cli/hook.rs index db64779..f6d8fa5 100644 --- a/src/cli/hook.rs +++ b/src/cli/hook.rs @@ -42,6 +42,16 @@ pub fn handle_hook(event: HookEvent, agent: &str) -> Result<()> { fs::create_dir_all(&pid_dir)?; fs::write(pid_dir.join(claude_pid.to_string()), &input.session_id)?; } + + // Generate a share slug and display the session URL + if let Ok(base) = LocalStorage::default_base_dir() { + let slug = nanoid::nanoid!(12); + let slug_dir = base.join("share-slugs"); + if fs::create_dir_all(&slug_dir).is_ok() { + let _ = fs::write(slug_dir.join(&input.session_id), &slug); + } + eprintln!("\u{1f9f5} https://threader.sh/s/{}", slug); + } } if matches!(event, HookEvent::SessionEnd) { if let Some(claude_pid) = crate::process::find_claude_ancestor_pid() { diff --git a/src/sync/uploader.rs b/src/sync/uploader.rs index 752574d..d04db05 100644 --- a/src/sync/uploader.rs +++ b/src/sync/uploader.rs @@ -220,6 +220,17 @@ impl BackgroundUploader { body["repo"] = serde_json::json!(repo); } + // Include client-generated share slug if available + if let Ok(base) = LocalStorage::default_base_dir() { + let slug_path = base.join("share-slugs").join(&entry.session_id); + if let Ok(slug) = fs::read_to_string(&slug_path) { + let slug = slug.trim(); + if !slug.is_empty() { + body["share_slug"] = serde_json::json!(slug); + } + } + } + let url = format!("{}/api/sessions", convex_site_url()); info!("Creating session {} at {}", entry.session_id, url);