Add desktop SMFS integration

This commit is contained in:
Sreeram Sreedhar 2026-06-22 17:38:06 -07:00
parent 1b7f59f88b
commit 7249b287e9
11 changed files with 1018 additions and 35 deletions

View file

@ -18,15 +18,27 @@ In development, the desktop UI and Rust auth commands default to the local API a
`http://localhost:8787`. Set `NEXT_PUBLIC_BACKEND_URL` / `SUPERMEMORY_API_URL`
if you need a different API.
SMFS integration uses the first available binary in this order:
1. `SUPERMEMORY_DESKTOP_SMFS_BIN`
2. bundled sidecar in `src-tauri/binaries`
3. `smfs` on `PATH`
The desktop-managed filesystem space uses container tags with the fixed
`sm_fs_` prefix. The default development tag is `sm_fs_desktop`.
## Build
```sh
bun run build # Next static export -> ./out (no Rust needed)
bun run tauri build # native .app / .dmg (requires the Rust toolchain)
bun run build:native # copy local smfs sidecar, then build native .app / .dmg
```
## Prerequisites for the native build
- Rust toolchain (`rustup`) + Xcode Command Line Tools.
- SMFS installed locally (`curl -fsSL https://smfs.ai/install | bash`) for
`bun run build:native`, or a pinned sidecar copied into `src-tauri/binaries`
by CI.
- App icons in `src-tauri/icons/` — generate once with
`bun run tauri icon <path-to-1024px.png>`.

View file

@ -11,13 +11,27 @@ import {
import { Button } from "@ui/components/button"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { useEffect, useState } from "react"
import { useCallback, useEffect, useState } from "react"
import { clearSession, getSession, type AuthSession } from "@/lib/auth"
import {
getSpotlightShortcut,
setSpotlightShortcut,
type SpotlightShortcut,
} from "@/lib/spotlight"
import {
getDefaultSmfsContainerTag,
getSmfsLogs,
getSmfsProfile,
getSmfsState,
mountSmfs,
onSmfsStatus,
revealSmfs,
SMFS_CONTAINER_TAG_PREFIX,
type SmfsProfile,
type SmfsStatus,
syncSmfs,
unmountSmfs,
} from "@/lib/smfs"
type AppInfo = {
name: string
@ -32,6 +46,21 @@ export default function SettingsPage() {
const [shortcut, setShortcut] = useState<SpotlightShortcut | null>(null)
const [shortcutError, setShortcutError] = useState<string | null>(null)
const [savingShortcut, setSavingShortcut] = useState<string | null>(null)
const [smfsStatuses, setSmfsStatuses] = useState<SmfsStatus[]>([])
const [smfsTag, setSmfsTag] = useState("sm_fs_desktop")
const [smfsError, setSmfsError] = useState<string | null>(null)
const [smfsBusy, setSmfsBusy] = useState<string | null>(null)
const [smfsLogs, setSmfsLogs] = useState<string | null>(null)
const [smfsProfile, setSmfsProfile] = useState<SmfsProfile | null>(null)
const refreshSmfsState = useCallback(async () => {
setSmfsError(null)
try {
setSmfsStatuses(await getSmfsState())
} catch (err) {
setSmfsError(formatError(err, "Could not load SMFS status"))
}
}, [])
useEffect(() => {
invoke<AppInfo>("app_info")
@ -43,6 +72,31 @@ export default function SettingsPage() {
getSpotlightShortcut()
.then(setShortcut)
.catch(() => setShortcut(null))
getDefaultSmfsContainerTag()
.then(setSmfsTag)
.catch(() => setSmfsTag("sm_fs_desktop"))
refreshSmfsState()
}, [refreshSmfsState])
useEffect(() => {
let unlisten: (() => void) | undefined
onSmfsStatus((event) => {
if (event.statuses.length > 0) {
setSmfsStatuses(event.statuses)
}
setSmfsError(event.error ?? null)
})
.then((handler) => {
unlisten = handler
})
.catch(() => {
unlisten = undefined
})
return () => {
unlisten?.()
}
}, [])
async function signOut() {
@ -68,6 +122,46 @@ export default function SettingsPage() {
}
}
async function runSmfsAction(
action: string,
fn: () => Promise<SmfsStatus | string | undefined>,
) {
setSmfsError(null)
setSmfsBusy(action)
try {
const result = await fn()
setSmfsProfile(null)
if (typeof result === "string") {
setSmfsLogs(result || "No logs available.")
} else if (result) {
setSmfsStatuses([result])
} else {
await refreshSmfsState()
}
} catch (err) {
setSmfsError(formatError(err, `Could not ${action}`))
} finally {
setSmfsBusy(null)
}
}
const smfsStatus = smfsStatuses[0]
const smfsSuffix = smfsTag.startsWith(SMFS_CONTAINER_TAG_PREFIX)
? smfsTag.slice(SMFS_CONTAINER_TAG_PREFIX.length)
: smfsTag
async function loadSmfsProfile() {
setSmfsError(null)
setSmfsBusy("read profile.md")
try {
setSmfsProfile(await getSmfsProfile(smfsTag))
} catch (err) {
setSmfsError(formatError(err, "Could not read profile.md"))
} finally {
setSmfsBusy(null)
}
}
return (
<div className="mx-auto w-full max-w-2xl p-8">
<h1 className="mb-6 font-semibold text-2xl">Settings</h1>
@ -127,6 +221,176 @@ export default function SettingsPage() {
</CardContent>
</Card>
<Card className="mb-4">
<CardHeader>
<CardTitle className="text-base">Memory filesystem</CardTitle>
<CardDescription>
Desktop-managed SMFS mount for a filesystem-backed space.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3 text-sm">
<label className="block space-y-1">
<span className="font-medium text-muted-foreground text-xs">
Filesystem space tag
</span>
<span className="flex overflow-hidden rounded-md border border-border bg-background">
<span className="flex shrink-0 items-center border-border border-r bg-muted/30 px-3 font-mono text-muted-foreground text-xs">
{SMFS_CONTAINER_TAG_PREFIX}
</span>
<input
value={smfsSuffix}
onChange={(event) =>
setSmfsTag(
`${SMFS_CONTAINER_TAG_PREFIX}${sanitizeSmfsSuffix(
event.target.value,
)}`,
)
}
disabled={smfsBusy !== null || smfsStatus?.ownedByApp}
className="min-w-0 flex-1 bg-transparent px-3 py-2 font-mono text-sm outline-none"
aria-label="SMFS container tag suffix"
/>
</span>
</label>
<Row label="Active tag" value={smfsStatus?.tag ?? smfsTag} />
<Row label="Required prefix" value={SMFS_CONTAINER_TAG_PREFIX} />
<Row label="State" value={formatSmfsState(smfsStatus?.state)} />
<Row label="Mount path" value={smfsStatus?.mountPath ?? "..."} />
<Row label="Binary" value={smfsStatus?.binaryPath ?? "..."} />
<Row
label="Ownership"
value={
smfsStatus?.ownedByApp
? "Managed by desktop"
: smfsStatus?.state === "external"
? "External mount"
: "Not mounted"
}
/>
<Row
label="profile.md"
value={
smfsStatus?.profileAvailable
? "Available"
: smfsStatus?.state === "mounted" ||
smfsStatus?.state === "external"
? "Not ready yet"
: "Unavailable"
}
/>
<div className="grid gap-2 sm:grid-cols-5">
<Button
type="button"
variant="outline"
disabled={smfsBusy !== null}
onClick={() =>
runSmfsAction("refresh SMFS", async () => {
await refreshSmfsState()
return undefined
})
}
>
Refresh
</Button>
<Button
type="button"
disabled={smfsBusy !== null || smfsStatus?.state === "external"}
onClick={() =>
runSmfsAction("mount SMFS", () => mountSmfs(smfsTag))
}
>
Mount
</Button>
<Button
type="button"
variant="outline"
disabled={smfsBusy !== null || !smfsStatus?.ownedByApp}
onClick={() =>
runSmfsAction("sync SMFS", () => syncSmfs(smfsTag))
}
>
Sync
</Button>
<Button
type="button"
variant="outline"
disabled={smfsBusy !== null}
onClick={() =>
runSmfsAction("reveal SMFS", async () => {
await revealSmfs(smfsTag)
return undefined
})
}
>
Reveal
</Button>
<Button
type="button"
variant="outline"
disabled={smfsBusy !== null || !smfsStatus?.ownedByApp}
onClick={() =>
runSmfsAction("unmount SMFS", () => unmountSmfs(smfsTag))
}
>
Unmount
</Button>
</div>
<Button
type="button"
variant="outline"
disabled={smfsBusy !== null}
onClick={() =>
runSmfsAction("read SMFS logs", () => getSmfsLogs(smfsTag, 200))
}
>
Load logs
</Button>
<Button
type="button"
variant="outline"
disabled={
smfsBusy !== null ||
!(
smfsStatus?.state === "mounted" ||
smfsStatus?.state === "external" ||
smfsStatus?.state === "degraded"
)
}
onClick={() => loadSmfsProfile()}
>
Read profile.md
</Button>
{smfsBusy ? (
<p className="text-muted-foreground text-xs">Working: {smfsBusy}</p>
) : null}
{(smfsError ?? smfsStatus?.error) ? (
<p className="text-destructive text-sm">
{smfsError ?? smfsStatus?.error}
</p>
) : null}
{smfsStatus?.state === "missing-binary" ? (
<p className="text-muted-foreground text-xs">
Install SMFS locally or set SUPERMEMORY_DESKTOP_SMFS_BIN to a
binary path while developing. Production builds should bundle the
sidecar in src-tauri/binaries.
</p>
) : null}
{smfsProfile ? (
<div className="space-y-2">
<Row label="Profile path" value={smfsProfile.path} />
<pre className="max-h-72 overflow-auto rounded-md border border-border bg-muted/20 p-3 whitespace-pre-wrap text-xs">
{smfsProfile.content}
</pre>
</div>
) : null}
{smfsLogs ? (
<pre className="max-h-52 overflow-auto rounded-md border border-border bg-muted/20 p-3 text-xs">
{smfsLogs}
</pre>
) : null}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">About</CardTitle>
@ -162,9 +426,11 @@ const SHORTCUT_OPTIONS = [
function Row({ label, value }: { label: string; value: string }) {
return (
<div className="flex justify-between">
<div className="grid grid-cols-[8rem_minmax(0,1fr)] gap-3">
<span className="text-muted-foreground">{label}</span>
<span className="font-medium tabular-nums">{value}</span>
<span className="truncate font-medium tabular-nums" title={value}>
{value}
</span>
</div>
)
}
@ -179,3 +445,34 @@ function formatShortcut(value: string) {
.replace("Shift", "⇧")
.replaceAll("+", " ")
}
function formatError(error: unknown, fallback: string) {
return error instanceof Error
? error.message
: typeof error === "string"
? error
: fallback
}
function sanitizeSmfsSuffix(value: string) {
return value.replace(/[^A-Za-z0-9_-]/g, "")
}
function formatSmfsState(state?: SmfsStatus["state"]) {
switch (state) {
case "degraded":
return "Mounted with warnings"
case "external":
return "External mount"
case "error":
return "Error"
case "missing-binary":
return "Missing binary"
case "mounted":
return "Mounted"
case "unmounted":
return "Unmounted"
default:
return "Loading"
}
}

73
apps/desktop/lib/smfs.ts Normal file
View file

@ -0,0 +1,73 @@
"use client"
import { invoke } from "@tauri-apps/api/core"
import { listen } from "@tauri-apps/api/event"
export type SmfsStatus = {
tag: string
state:
| "external"
| "mounted"
| "degraded"
| "unmounted"
| "missing-binary"
| "error"
mountPath: string
ownedByApp: boolean
profileAvailable: boolean
lastSync?: string | null
binaryPath: string
error?: string | null
}
export type SmfsStatusEvent = {
statuses: SmfsStatus[]
error?: string | null
}
export type SmfsProfile = {
tag: string
path: string
content: string
}
export const SMFS_CONTAINER_TAG_PREFIX = "sm_fs_"
export const SMFS_STATUS_EVENT = "smfs:status"
export function getSmfsState() {
return invoke<SmfsStatus[]>("smfs_state")
}
export function mountSmfs(tag?: string) {
return invoke<SmfsStatus>("smfs_mount", { tag })
}
export function unmountSmfs(tag?: string) {
return invoke<SmfsStatus>("smfs_unmount", { tag })
}
export function syncSmfs(tag?: string) {
return invoke<SmfsStatus>("smfs_sync", { tag })
}
export function revealSmfs(tag?: string) {
return invoke<void>("smfs_reveal", { tag })
}
export function getSmfsLogs(tag?: string, lines = 200) {
return invoke<string>("smfs_logs", { tag, lines })
}
export function getDefaultSmfsContainerTag() {
return invoke<string>("smfs_default_container_tag")
}
export function getSmfsProfile(tag?: string) {
return invoke<SmfsProfile>("smfs_profile", { tag })
}
export function onSmfsStatus(handler: (event: SmfsStatusEvent) => void) {
return listen<SmfsStatusEvent>(SMFS_STATUS_EVENT, (event) => {
handler(event.payload)
})
}

View file

@ -5,6 +5,8 @@
"scripts": {
"dev": "NEXT_PUBLIC_DESKTOP_DEV=1 NEXT_PUBLIC_BACKEND_URL=http://localhost:8787 next dev --port 3003",
"build": "next build",
"prepare-smfs-sidecar": "mkdir -p src-tauri/binaries && cp \"$(command -v smfs)\" \"src-tauri/binaries/smfs-$(rustc -vV | sed -n 's/^host: //p')\"",
"build:native": "bun run prepare-smfs-sidecar && tauri build",
"check-types": "tsc --noEmit",
"tauri": "tauri"
},

View file

@ -3,3 +3,7 @@
# Tauri-generated schemas / artifacts
/gen/schemas
# SMFS sidecars are copied or downloaded per target before native packaging.
/binaries/*
!/binaries/.gitkeep

View file

@ -0,0 +1 @@

View file

@ -46,7 +46,7 @@ fn get_cached_token() -> Result<Option<String>, String> {
.map_err(|_| "Could not lock token cache".to_string())
}
fn api_url() -> String {
pub fn api_url() -> String {
if let Ok(api_url) = std::env::var("SUPERMEMORY_DESKTOP_API_URL") {
return api_url;
}
@ -139,7 +139,9 @@ pub async fn whoami() -> Result<AuthSession, String> {
fn first_string(value: &Value, paths: &[&[&str]]) -> Option<String> {
paths.iter().find_map(|path| {
let found = path.iter().try_fold(value, |current, key| current.get(key))?;
let found = path
.iter()
.try_fold(value, |current, key| current.get(key))?;
found.as_str().map(ToString::to_string)
})
}

View file

@ -1,4 +1,5 @@
mod auth;
mod smfs;
mod spotlight;
mod tray;
@ -80,6 +81,50 @@ fn spotlight_set_shortcut(
spotlight::set_shortcut(&app, &state, accelerator)
}
#[tauri::command]
fn smfs_state(app: tauri::AppHandle) -> Result<Vec<smfs::SmfsStatus>, String> {
smfs::state(&app)
}
#[tauri::command]
fn smfs_mount(app: tauri::AppHandle, tag: Option<String>) -> Result<smfs::SmfsStatus, String> {
smfs::mount(&app, tag)
}
#[tauri::command]
fn smfs_unmount(app: tauri::AppHandle, tag: Option<String>) -> Result<smfs::SmfsStatus, String> {
smfs::unmount(&app, tag)
}
#[tauri::command]
fn smfs_sync(app: tauri::AppHandle, tag: Option<String>) -> Result<smfs::SmfsStatus, String> {
smfs::sync(&app, tag)
}
#[tauri::command]
fn smfs_reveal(app: tauri::AppHandle, tag: Option<String>) -> Result<(), String> {
smfs::reveal(&app, tag)
}
#[tauri::command]
fn smfs_logs(
app: tauri::AppHandle,
tag: Option<String>,
lines: Option<u32>,
) -> Result<String, String> {
smfs::logs(&app, tag, lines)
}
#[tauri::command]
fn smfs_default_container_tag() -> String {
smfs::default_container_tag().to_string()
}
#[tauri::command]
fn smfs_profile(app: tauri::AppHandle, tag: Option<String>) -> Result<smfs::SmfsProfile, String> {
smfs::profile(&app, tag)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
@ -90,6 +135,7 @@ pub fn run() {
app.manage(spotlight::SpotlightShortcutState::new(accelerator.clone()));
spotlight::register_shortcut(app, &accelerator);
tray::create(app)?;
smfs::start_status_poller(app.handle().clone());
Ok(())
})
.invoke_handler(tauri::generate_handler![
@ -102,7 +148,15 @@ pub fn run() {
spotlight_hide,
spotlight_open_result,
spotlight_get_shortcut,
spotlight_set_shortcut
spotlight_set_shortcut,
smfs_state,
smfs_mount,
smfs_unmount,
smfs_sync,
smfs_reveal,
smfs_logs,
smfs_default_container_tag,
smfs_profile
])
// Bootstrap failure is unrecoverable (no window, no app), so we abort
// loudly here. This is the one sanctioned `expect` — see roadmap quality bar.

View file

@ -0,0 +1,543 @@
use std::{
env, fs,
path::{Path, PathBuf},
process::{Command, Output},
thread,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tauri::{AppHandle, Emitter, Manager};
use crate::auth;
pub const CONTAINER_TAG_PREFIX: &str = "sm_fs_";
const DEFAULT_CONTAINER_TAG: &str = "sm_fs_desktop";
const STATUS_EVENT: &str = "smfs:status";
const STATUS_POLL_INTERVAL: Duration = Duration::from_secs(10);
const OWNERSHIP_DIR: &str = "smfs-owned-mounts";
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SmfsStatus {
tag: String,
state: SmfsState,
mount_path: String,
owned_by_app: bool,
profile_available: bool,
last_sync: Option<String>,
binary_path: String,
error: Option<String>,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "kebab-case")]
enum SmfsState {
External,
Mounted,
Degraded,
Unmounted,
MissingBinary,
Error,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SmfsStatusEvent {
statuses: Vec<SmfsStatus>,
error: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SmfsProfile {
tag: String,
path: String,
content: String,
}
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct OwnershipMarker {
tag: String,
mount_path: String,
created_at_unix: u64,
}
pub fn default_container_tag() -> &'static str {
DEFAULT_CONTAINER_TAG
}
pub fn start_status_poller(app: AppHandle) {
thread::spawn(move || loop {
let _ = emit_status(&app);
thread::sleep(STATUS_POLL_INTERVAL);
});
}
pub fn emit_status(app: &AppHandle) -> Result<(), String> {
let event = match state(app) {
Ok(statuses) => SmfsStatusEvent {
statuses,
error: None,
},
Err(error) => SmfsStatusEvent {
statuses: Vec::new(),
error: Some(error),
},
};
app.emit(STATUS_EVENT, event)
.map_err(|error| error.to_string())
}
pub fn state(app: &AppHandle) -> Result<Vec<SmfsStatus>, String> {
Ok(vec![status_for_tag(app, DEFAULT_CONTAINER_TAG)?])
}
pub fn mount(app: &AppHandle, tag: Option<String>) -> Result<SmfsStatus, String> {
let tag = normalize_tag(tag)?;
let token = auth::get_token()?.ok_or_else(|| "No stored token".to_string())?;
let mount_path = expected_mount_path(app, &tag)?;
fs::create_dir_all(&mount_path).map_err(|error| error.to_string())?;
let status = status_for_tag(app, &tag)?;
match status.state {
SmfsState::External => {
return Err(format!(
"{tag} is already mounted outside Supermemory Desktop; leaving it untouched"
));
}
SmfsState::Mounted | SmfsState::Degraded if status.owned_by_app => {
return Ok(status);
}
SmfsState::Mounted | SmfsState::Degraded => {
return Err(format!(
"{tag} is already mounted but is not marked as app-owned; leaving it untouched"
));
}
SmfsState::MissingBinary => {
return Err(status
.error
.unwrap_or_else(|| "Could not find the smfs binary".to_string()));
}
SmfsState::Error | SmfsState::Unmounted => {}
}
let mount_path_string = path_to_string(&mount_path);
let output = run_smfs(
app,
[
"mount",
&tag,
"--path",
&mount_path_string,
"--key",
&token,
"--api-url",
&auth::api_url(),
"--sync-interval",
"30",
],
)?;
ensure_success(output)?;
write_ownership_marker(app, &tag, &mount_path)?;
let status = status_for_tag(app, &tag)?;
let _ = emit_status(app);
Ok(status)
}
pub fn unmount(app: &AppHandle, tag: Option<String>) -> Result<SmfsStatus, String> {
let tag = normalize_tag(tag)?;
let status = status_for_tag(app, &tag)?;
if matches!(status.state, SmfsState::Unmounted) {
let _ = remove_ownership_marker(app, &tag);
return Ok(status);
}
if !status.owned_by_app {
return Err(format!(
"{tag} is not an app-owned SMFS mount; refusing to unmount it"
));
}
let output = run_smfs(app, ["unmount", &tag])?;
ensure_success(output)?;
remove_ownership_marker(app, &tag)?;
let status = status_for_tag(app, &tag)?;
let _ = emit_status(app);
Ok(status)
}
pub fn sync(app: &AppHandle, tag: Option<String>) -> Result<SmfsStatus, String> {
let tag = normalize_tag(tag)?;
let status = status_for_tag(app, &tag)?;
if !matches!(
status.state,
SmfsState::Mounted | SmfsState::Degraded | SmfsState::External
) {
return Err(format!("{tag} is not mounted"));
}
let output = run_smfs(app, ["sync", &tag])?;
ensure_success(output)?;
let status = status_for_tag(app, &tag)?;
let _ = emit_status(app);
Ok(status)
}
pub fn reveal(app: &AppHandle, tag: Option<String>) -> Result<(), String> {
let tag = normalize_tag(tag)?;
let status = status_for_tag(app, &tag)?;
let path = if matches!(
status.state,
SmfsState::Mounted | SmfsState::Degraded | SmfsState::External
) {
PathBuf::from(status.mount_path)
} else {
expected_mount_path(app, &tag)?
};
fs::create_dir_all(&path).map_err(|error| error.to_string())?;
let output = Command::new("open")
.arg(path)
.output()
.map_err(|error| format!("Could not open Finder: {error}"))?;
ensure_success(output)
}
pub fn logs(app: &AppHandle, tag: Option<String>, lines: Option<u32>) -> Result<String, String> {
let tag = normalize_tag(tag)?;
let line_count = lines.unwrap_or(200).clamp(1, 1000).to_string();
let output = run_smfs(app, ["logs", "-n", &line_count, &tag])?;
output_text(output)
}
pub fn profile(app: &AppHandle, tag: Option<String>) -> Result<SmfsProfile, String> {
let tag = normalize_tag(tag)?;
let status = status_for_tag(app, &tag)?;
if !matches!(
status.state,
SmfsState::Mounted | SmfsState::Degraded | SmfsState::External
) {
return Err(format!("{tag} is not mounted"));
}
let profile_path = PathBuf::from(status.mount_path).join("profile.md");
let content = fs::read_to_string(&profile_path)
.map_err(|error| format!("Could not read profile.md: {error}"))?;
Ok(SmfsProfile {
tag,
path: path_to_string(&profile_path),
content,
})
}
fn status_for_tag(app: &AppHandle, tag: &str) -> Result<SmfsStatus, String> {
validate_tag(tag)?;
let expected_mount_path = expected_mount_path(app, tag)?;
let binary_path = smfs_bin(app);
let binary_path_string = path_to_string(&binary_path);
let output = match run_smfs(app, ["status", tag, "--json"]) {
Ok(output) => output,
Err(error) if is_missing_binary_error(&error) => {
return Ok(SmfsStatus {
tag: tag.to_string(),
state: SmfsState::MissingBinary,
mount_path: path_to_string(&expected_mount_path),
owned_by_app: false,
profile_available: false,
last_sync: None,
binary_path: binary_path_string,
error: Some(error),
});
}
Err(error) => {
return Ok(SmfsStatus {
tag: tag.to_string(),
state: SmfsState::Error,
mount_path: path_to_string(&expected_mount_path),
owned_by_app: false,
profile_available: false,
last_sync: None,
binary_path: binary_path_string,
error: Some(error),
});
}
};
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if output.status.success() {
let parsed = serde_json::from_str::<Value>(&stdout).unwrap_or(Value::Null);
let active_mount_path = first_string(
&parsed,
&[
&["mountPath"],
&["mount_path"],
&["path"],
&["mount", "path"],
&["mount", "mountPath"],
],
)
.map(PathBuf::from)
.unwrap_or_else(|| expected_mount_path.clone());
let owned_by_app = is_app_owned_mount(app, tag, &active_mount_path);
let profile_available = active_mount_path.join("profile.md").exists();
let state = if is_degraded(&parsed) {
SmfsState::Degraded
} else if owned_by_app {
SmfsState::Mounted
} else {
SmfsState::External
};
return Ok(SmfsStatus {
tag: tag.to_string(),
state,
mount_path: path_to_string(&active_mount_path),
owned_by_app,
profile_available,
last_sync: first_string(&parsed, &[&["lastSync"], &["last_sync"]]),
binary_path: binary_path_string,
error: None,
});
}
let message = format!("{stdout}{stderr}");
if message.contains("daemon not running")
|| message.contains("no socket")
|| message.contains("not mounted")
{
return Ok(SmfsStatus {
tag: tag.to_string(),
state: SmfsState::Unmounted,
mount_path: path_to_string(&expected_mount_path),
owned_by_app: false,
profile_available: false,
last_sync: None,
binary_path: binary_path_string,
error: None,
});
}
Ok(SmfsStatus {
tag: tag.to_string(),
state: SmfsState::Error,
mount_path: path_to_string(&expected_mount_path),
owned_by_app: false,
profile_available: false,
last_sync: None,
binary_path: binary_path_string,
error: Some(message.trim().to_string()),
})
}
fn normalize_tag(tag: Option<String>) -> Result<String, String> {
let tag = tag.unwrap_or_else(|| DEFAULT_CONTAINER_TAG.to_string());
validate_tag(&tag)?;
Ok(tag)
}
fn validate_tag(tag: &str) -> Result<(), String> {
if !tag.starts_with(CONTAINER_TAG_PREFIX) {
return Err(format!(
"SMFS desktop container tags must start with {CONTAINER_TAG_PREFIX}"
));
}
if tag.len() <= CONTAINER_TAG_PREFIX.len() {
return Err("SMFS desktop container tag needs a suffix after sm_fs_".to_string());
}
if tag
.chars()
.all(|character| character.is_ascii_alphanumeric() || character == '_' || character == '-')
{
Ok(())
} else {
Err("SMFS desktop container tags can only contain letters, numbers, underscores, and hyphens".to_string())
}
}
fn expected_mount_path(app: &AppHandle, tag: &str) -> Result<PathBuf, String> {
Ok(app
.path()
.app_data_dir()
.map_err(|error| error.to_string())?
.join("mounts")
.join(tag))
}
fn ownership_marker_path(app: &AppHandle, tag: &str) -> Result<PathBuf, String> {
Ok(app
.path()
.app_config_dir()
.map_err(|error| error.to_string())?
.join(OWNERSHIP_DIR)
.join(format!("{tag}.json")))
}
fn write_ownership_marker(app: &AppHandle, tag: &str, mount_path: &Path) -> Result<(), String> {
let marker_path = ownership_marker_path(app, tag)?;
let parent = marker_path
.parent()
.ok_or_else(|| "Could not resolve SMFS ownership directory".to_string())?;
fs::create_dir_all(parent).map_err(|error| error.to_string())?;
let created_at_unix = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|error| error.to_string())?
.as_secs();
let payload = serde_json::to_vec_pretty(&OwnershipMarker {
tag: tag.to_string(),
mount_path: path_to_string(mount_path),
created_at_unix,
})
.map_err(|error| error.to_string())?;
let temp_path = marker_path.with_extension("json.tmp");
fs::write(&temp_path, payload).map_err(|error| error.to_string())?;
fs::rename(temp_path, marker_path).map_err(|error| error.to_string())
}
fn remove_ownership_marker(app: &AppHandle, tag: &str) -> Result<(), String> {
let marker_path = ownership_marker_path(app, tag)?;
match fs::remove_file(marker_path) {
Ok(()) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(error.to_string()),
}
}
fn is_app_owned_mount(app: &AppHandle, tag: &str, active_mount_path: &Path) -> bool {
let Some(marker) = read_ownership_marker(app, tag) else {
return false;
};
marker.tag == tag && PathBuf::from(marker.mount_path) == active_mount_path
}
fn read_ownership_marker(app: &AppHandle, tag: &str) -> Option<OwnershipMarker> {
let path = ownership_marker_path(app, tag).ok()?;
let contents = fs::read_to_string(path).ok()?;
serde_json::from_str::<OwnershipMarker>(&contents).ok()
}
fn run_smfs<const N: usize>(app: &AppHandle, args: [&str; N]) -> Result<Output, String> {
let binary = smfs_bin(app);
Command::new(&binary)
.args(args)
.env("SUPERMEMORY_API_URL", auth::api_url())
.output()
.map_err(|error| format!("Could not run smfs at {}: {error}", path_to_string(&binary)))
}
fn smfs_bin(app: &AppHandle) -> PathBuf {
if let Ok(path) = env::var("SUPERMEMORY_DESKTOP_SMFS_BIN") {
return PathBuf::from(path);
}
for candidate in smfs_sidecar_candidates(app) {
if candidate.is_file() {
return candidate;
}
}
find_in_path("smfs").unwrap_or_else(|| PathBuf::from("smfs"))
}
fn smfs_sidecar_candidates(app: &AppHandle) -> Vec<PathBuf> {
let mut candidates = Vec::new();
if let Ok(resource_dir) = app.path().resource_dir() {
candidates.push(resource_dir.join("binaries").join("smfs"));
candidates.push(resource_dir.join("smfs"));
candidates.push(resource_dir.join("binaries").join(sidecar_name()));
candidates.push(resource_dir.join(sidecar_name()));
}
if let Ok(exe_path) = env::current_exe() {
if let Some(exe_dir) = exe_path.parent() {
candidates.push(exe_dir.join("binaries").join("smfs"));
candidates.push(exe_dir.join("smfs"));
candidates.push(exe_dir.join("binaries").join(sidecar_name()));
candidates.push(exe_dir.join(sidecar_name()));
}
}
candidates
}
fn sidecar_name() -> String {
format!("smfs-{}", target_triple())
}
fn target_triple() -> String {
let arch = env::consts::ARCH;
let os = match env::consts::OS {
"macos" => "apple-darwin",
"linux" => "unknown-linux-gnu",
other => other,
};
format!("{arch}-{os}")
}
fn find_in_path(binary_name: &str) -> Option<PathBuf> {
let path = env::var_os("PATH")?;
env::split_paths(&path)
.map(|entry| entry.join(binary_name))
.find(|candidate| candidate.is_file())
}
fn ensure_success(output: Output) -> Result<(), String> {
if output.status.success() {
Ok(())
} else {
Err(output_text(output)?)
}
}
fn output_text(output: Output) -> Result<String, String> {
let stdout = String::from_utf8(output.stdout).map_err(|error| error.to_string())?;
let stderr = String::from_utf8(output.stderr).map_err(|error| error.to_string())?;
let text = format!("{stdout}{stderr}");
Ok(text.trim().to_string())
}
fn path_to_string(path: &Path) -> String {
path.to_string_lossy().into_owned()
}
fn first_string(value: &Value, paths: &[&[&str]]) -> Option<String> {
paths.iter().find_map(|path| {
let found = path
.iter()
.try_fold(value, |current, key| current.get(key))?;
found.as_str().map(ToString::to_string)
})
}
fn is_degraded(value: &Value) -> bool {
first_string(
value,
&[
&["state"],
&["status"],
&["health"],
&["mount", "state"],
&["mount", "status"],
],
)
.is_some_and(|state| state.eq_ignore_ascii_case("degraded"))
}
fn is_missing_binary_error(error: &str) -> bool {
error.contains("No such file or directory") || error.contains("os error 2")
}

View file

@ -2,12 +2,9 @@ use std::{fs, sync::Mutex};
use serde::{Deserialize, Serialize};
use tauri::{
App, AppHandle, Emitter, Manager, WebviewUrl, WebviewWindow, WebviewWindowBuilder,
WindowEvent,
};
use tauri_plugin_global_shortcut::{
GlobalShortcutExt, Shortcut, ShortcutEvent, ShortcutState,
App, AppHandle, Emitter, Manager, WebviewUrl, WebviewWindow, WebviewWindowBuilder, WindowEvent,
};
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutEvent, ShortcutState};
const MAIN_LABEL: &str = "main";
const SPOTLIGHT_LABEL: &str = "spotlight";
@ -59,20 +56,17 @@ pub fn create_window(app: &App) -> tauri::Result<WebviewWindow> {
return Ok(window);
}
let window = WebviewWindowBuilder::new(
app,
SPOTLIGHT_LABEL,
WebviewUrl::App("spotlight/".into()),
)
.title("Supermemory Spotlight")
.inner_size(760.0, 420.0)
.min_inner_size(560.0, 220.0)
.decorations(false)
.always_on_top(true)
.skip_taskbar(true)
.visible(false)
.center()
.build()?;
let window =
WebviewWindowBuilder::new(app, SPOTLIGHT_LABEL, WebviewUrl::App("spotlight/".into()))
.title("Supermemory Spotlight")
.inner_size(760.0, 420.0)
.min_inner_size(560.0, 220.0)
.decorations(false)
.always_on_top(true)
.skip_taskbar(true)
.visible(false)
.center()
.build()?;
let window_for_blur = window.clone();
window.on_window_event(move |event| {
@ -194,18 +188,18 @@ where
}
fn unregister_accelerator(app: &AppHandle, accelerator: &str) -> Result<(), String> {
validate_accelerator(accelerator)?;
app.global_shortcut()
.unregister(accelerator)
.map_err(|error| error.to_string())
validate_accelerator(accelerator)?;
app.global_shortcut()
.unregister(accelerator)
.map_err(|error| error.to_string())
}
fn unregister_accelerator_if_registered(app: &AppHandle, accelerator: &str) -> Result<(), String> {
validate_accelerator(accelerator)?;
if app.global_shortcut().is_registered(accelerator) {
unregister_accelerator(app, accelerator)?;
}
Ok(())
validate_accelerator(accelerator)?;
if app.global_shortcut().is_registered(accelerator) {
unregister_accelerator(app, accelerator)?;
}
Ok(())
}
fn validate_accelerator(accelerator: &str) -> Result<(), String> {

View file

@ -29,6 +29,7 @@
"bundle": {
"active": true,
"targets": ["app", "dmg"],
"externalBin": ["binaries/smfs"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",