diff --git a/Cargo.lock b/Cargo.lock
index 09d4db1d..a6fa5093 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -17,6 +17,20 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "const-random",
+ "getrandom 0.3.4",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
[[package]]
name = "aho-corasick"
version = "1.1.4"
@@ -44,6 +58,21 @@ dependencies = [
"equator",
]
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
[[package]]
name = "alloca"
version = "0.4.0"
@@ -308,6 +337,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+[[package]]
+name = "base64"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
+
[[package]]
name = "bit-set"
version = "0.8.0"
@@ -386,6 +421,27 @@ dependencies = [
"objc2",
]
+[[package]]
+name = "brotli"
+version = "8.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "5.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
[[package]]
name = "built"
version = "0.8.1"
@@ -524,7 +580,7 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e"
dependencies = [
- "base64",
+ "base64 0.22.1",
"encoding_rs",
]
@@ -642,6 +698,26 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
+[[package]]
+name = "const-random"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
+dependencies = [
+ "const-random-macro",
+]
+
+[[package]]
+name = "const-random-macro"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
+dependencies = [
+ "getrandom 0.2.17",
+ "once_cell",
+ "tiny-keccak",
+]
+
[[package]]
name = "const-serialize"
version = "0.7.2"
@@ -1217,7 +1293,7 @@ checksum = "236deb81f1a04de704fb80cf7838c65607441c76914de2e036709540c540a39d"
dependencies = [
"anyhow",
"async-trait",
- "base64",
+ "base64 0.22.1",
"bytes",
"cocoa",
"core-foundation",
@@ -1325,7 +1401,7 @@ dependencies = [
"async-tungstenite",
"axum",
"axum-core",
- "base64",
+ "base64 0.22.1",
"bytes",
"ciborium",
"const-str",
@@ -1379,7 +1455,7 @@ checksum = "0011e9bca8da2ae6bf02ba9ef93ba6c5fa539e402de3aeb316ad86ca77c59079"
dependencies = [
"anyhow",
"axum-core",
- "base64",
+ "base64 0.22.1",
"ciborium",
"dioxus-core",
"dioxus-document",
@@ -1640,6 +1716,15 @@ dependencies = [
"syn 2.0.119",
]
+[[package]]
+name = "dlib"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a"
+dependencies = [
+ "libloading 0.7.4",
+]
+
[[package]]
name = "dlopen2"
version = "0.8.2"
@@ -1672,6 +1757,12 @@ dependencies = [
"litrs",
]
+[[package]]
+name = "downcast-rs"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+
[[package]]
name = "dpi"
version = "0.1.2"
@@ -1855,6 +1946,7 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
+ "zlib-rs",
]
[[package]]
@@ -2471,6 +2563,7 @@ checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
+ "num-traits",
"zerocopy",
]
@@ -2507,7 +2600,7 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb"
dependencies = [
- "base64",
+ "base64 0.22.1",
"bytes",
"headers-core",
"http",
@@ -2645,7 +2738,7 @@ version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
- "base64",
+ "base64 0.22.1",
"bytes",
"futures-channel",
"futures-util",
@@ -3269,6 +3362,15 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+[[package]]
+name = "lz4_flex"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecbdfe44b1bd960b68170b417450a628c43f7cf56bb3c5317e61cb230ee7f226"
+dependencies = [
+ "twox-hash",
+]
+
[[package]]
name = "mac"
version = "0.1.1"
@@ -3621,6 +3723,16 @@ dependencies = [
"num-traits",
]
+[[package]]
+name = "num-bigint"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
[[package]]
name = "num-complex"
version = "0.4.6"
@@ -3663,7 +3775,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
- "num-bigint",
+ "num-bigint 0.4.8",
"num-integer",
"num-traits",
]
@@ -3675,6 +3787,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
+ "libm",
]
[[package]]
@@ -3942,6 +4055,32 @@ dependencies = [
"windows-link 0.2.1",
]
+[[package]]
+name = "parquet"
+version = "59.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7065842956a20c2a536924ce8e4d9955f7422451511b9eb7500d7bfe5077e59c"
+dependencies = [
+ "ahash",
+ "base64 0.23.1",
+ "brotli",
+ "bytes",
+ "chrono",
+ "crc32fast",
+ "flate2",
+ "half",
+ "hashbrown 0.17.1",
+ "lz4_flex",
+ "num-bigint 0.5.1",
+ "num-integer",
+ "num-traits",
+ "seq-macro",
+ "simdutf8",
+ "snap",
+ "twox-hash",
+ "zstd",
+]
+
[[package]]
name = "paste"
version = "1.0.15"
@@ -4358,6 +4497,15 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+[[package]]
+name = "quick-xml"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "quinn"
version = "0.11.11"
@@ -4732,7 +4880,7 @@ version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
- "base64",
+ "base64 0.22.1",
"bytes",
"cookie",
"cookie_store",
@@ -4790,6 +4938,9 @@ dependencies = [
"raw-window-handle 0.6.2",
"wasm-bindgen",
"wasm-bindgen-futures",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
"web-sys",
"windows-sys 0.61.2",
]
@@ -5030,6 +5181,12 @@ dependencies = [
"futures-core",
]
+[[package]]
+name = "seq-macro"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
+
[[package]]
name = "serde"
version = "1.0.229"
@@ -5288,9 +5445,12 @@ version = "0.1.0"
dependencies = [
"chrono",
"dioxus",
+ "dirs",
"futures-util",
"js-sys",
+ "parquet",
"reqwest",
+ "rfd",
"rusqlite",
"serde",
"serde_json",
@@ -5354,6 +5514,12 @@ version = "1.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+[[package]]
+name = "snap"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "199905e6153d6405f9728fe44daace35f8f837bbf830bb6e85fbd5828709a886"
+
[[package]]
name = "socket2"
version = "0.6.5"
@@ -5724,6 +5890,15 @@ dependencies = [
"time-core",
]
+[[package]]
+name = "tiny-keccak"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
+dependencies = [
+ "crunchy",
+]
+
[[package]]
name = "tinystr"
version = "0.8.3"
@@ -6067,6 +6242,12 @@ dependencies = [
"utf-8",
]
+[[package]]
+name = "twox-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8464ec13c3691491391d9fce00f6416c9a48e46972f72d7865688be2080192c9"
+
[[package]]
name = "type-map"
version = "0.5.1"
@@ -6344,6 +6525,66 @@ dependencies = [
"web-sys",
]
+[[package]]
+name = "wayland-backend"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "016ccf01d1c58b6f8999612813e17c9b2390f7d70671428869913310f83f54b8"
+dependencies = [
+ "cc",
+ "downcast-rs",
+ "rustix",
+ "scoped-tls",
+ "smallvec",
+ "wayland-sys",
+]
+
+[[package]]
+name = "wayland-client"
+version = "0.31.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073"
+dependencies = [
+ "bitflags 2.13.1",
+ "rustix",
+ "wayland-backend",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols"
+version = "0.32.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6"
+dependencies = [
+ "bitflags 2.13.1",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-scanner"
+version = "0.31.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0"
+dependencies = [
+ "proc-macro2",
+ "quick-xml",
+ "quote",
+]
+
+[[package]]
+name = "wayland-sys"
+version = "0.31.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"
+dependencies = [
+ "dlib",
+ "log",
+ "pkg-config",
+]
+
[[package]]
name = "web-sys"
version = "0.3.103"
@@ -6939,7 +7180,7 @@ version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2"
dependencies = [
- "base64",
+ "base64 0.22.1",
"block2",
"cookie",
"crossbeam-channel",
@@ -7136,6 +7377,12 @@ dependencies = [
"syn 2.0.119",
]
+[[package]]
+name = "zlib-rs"
+version = "0.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
+
[[package]]
name = "zmij"
version = "1.0.23"
diff --git a/assets/icons/line/settings.svg b/assets/icons/line/settings.svg
new file mode 100644
index 00000000..01a76ccf
--- /dev/null
+++ b/assets/icons/line/settings.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/crates/sl-viewer/Cargo.toml b/crates/sl-viewer/Cargo.toml
index f22ba4a6..a61790b2 100644
--- a/crates/sl-viewer/Cargo.toml
+++ b/crates/sl-viewer/Cargo.toml
@@ -21,15 +21,28 @@ rusqlite = { version = "0.40", optional = true }
zstd = { version = "0.13", optional = true }
wasm-bindgen = { version = "0.2", optional = true }
web-sys = { version = "0.3", optional = true, features = ["Document", "Element", "Event", "EventSource", "HtmlElement", "KeyboardEvent", "Location", "MessageEvent", "UrlSearchParams", "Window"] }
+# parquet feature: ingest Claude conversation parquet exports under ~/.claude/projects.
+# Uses the row-based API (no arrow) for minimal dependency footprint.
+parquet = { version = "59", optional = true, default-features = false, features = ["snap", "zstd", "brotli", "flate2-zlib-rs", "lz4", "base64", "simdutf8", "crc"] }
+# Native folder-picker dialog for the "Pick folder…" affordance (desktop only).
+# Uses `rfd`'s cocoa backend on macOS, native Win32 dialogs on Windows, and
+# the XDG-portal/GTK3 stack on Linux. Default features are required on
+# Linux so the dialog can talk to a portal service.
+rfd = { version = "0.17", optional = true }
+# Resolve the platform config directory for persisted corpus paths.
+dirs = "6"
[dev-dependencies]
rusqlite = "0.40"
zstd = "0.13"
tempfile = "3"
+# Pull parquet into the test build so fixture writers and unit tests can exercise
+# the parquet source even when the production `parquet` feature is disabled.
+parquet = { version = "59", default-features = false, features = ["snap", "zstd", "brotli", "flate2-zlib-rs", "lz4", "base64", "simdutf8", "crc"] }
[features]
default = ["desktop"]
-desktop = ["dioxus/desktop", "dep:reqwest", "dep:tokio", "dep:tokio-util", "dep:futures-util", "dep:chrono", "compressed-sessions"]
+desktop = ["dioxus/desktop", "dep:reqwest", "dep:tokio", "dep:tokio-util", "dep:futures-util", "dep:chrono", "dep:rfd", "compressed-sessions"]
compressed-sessions = ["dep:zstd", "session-ledger/compress"]
web = ["dioxus/web", "dep:reqwest", "dep:futures-util", "dep:js-sys", "dep:wasm-bindgen", "dep:web-sys"]
# Enable loading real Forge SQLite sessions in the viewer.
@@ -39,6 +52,10 @@ sqlite = [
"dep:zstd",
"session-ledger/sqlite",
]
+# Enable ingesting Claude conversation parquet exports (e.g. files dropped under
+# `~/.claude/projects/*.parquet`). Without this feature, parquet files are
+# silently skipped by the auto-discovery loader.
+parquet = ["dep:parquet"]
[lints.rust]
unsafe_code = "forbid"
diff --git a/crates/sl-viewer/src/app.rs b/crates/sl-viewer/src/app.rs
index 27a66146..942f5fe4 100644
--- a/crates/sl-viewer/src/app.rs
+++ b/crates/sl-viewer/src/app.rs
@@ -7,8 +7,9 @@ use session_ledger::domain::{
use crate::async_states::{ErrorColorFixture, ErrorState, FirstRunEmpty, LoadingState};
use crate::bundle_diff::{BundleDiff, OkfBundle};
use crate::bundle_list::{summarize, BundleSummary};
+use crate::cli_help;
use crate::command_palette::{CommandPalette, PaletteAction};
-use crate::corpus_loader::{load_sessions, DataSource};
+use crate::corpus_loader::{load_sessions_with_custom, CustomCorpusPath, DataSource};
use crate::corpus_tab::CorpusTab;
use crate::detail_pane::{extract_detail, BundleDetail};
use crate::fixture::visual_fixture_active;
@@ -20,7 +21,9 @@ use crate::memory_tab::MemoryWiki;
use crate::replay_view::ReplayView;
use crate::search_view::SearchView;
use crate::session_transcript::SessionTranscript;
-use crate::theme::ThemeColors;
+use crate::settings::{DefaultTab, Settings};
+use crate::settings_tab::SettingsTab;
+use crate::theme::{Theme, ThemeColors};
use crate::timeline::TimelineView;
use crate::tokens::{TOKENS_CSS, VIEWER_COLOR_SCHEME};
use crate::unfinished_tab::UnfinishedWork;
@@ -40,10 +43,13 @@ enum Tab {
/// other tab derives from, so users can see what was discovered and reload
/// discovery on demand. (FR-RAW-1)
Corpus,
+ /// Persistent user preferences (theme, default tab, daemon URL, version).
+ /// (FR-VIEWER-SETTINGS-1)
+ Settings,
}
impl Tab {
- const ALL: [Tab; 9] = [
+ const ALL: [Tab; 10] = [
Tab::Bundles,
Tab::History,
Tab::Unfinished,
@@ -53,6 +59,7 @@ impl Tab {
Tab::Timeline,
Tab::Replay,
Tab::Corpus,
+ Tab::Settings,
];
fn label(self) -> &'static str {
@@ -66,6 +73,7 @@ impl Tab {
Tab::Timeline => "Timeline",
Tab::Replay => "Replay",
Tab::Corpus => "Raw Sessions",
+ Tab::Settings => "Settings",
}
}
@@ -80,6 +88,7 @@ impl Tab {
Tab::Timeline => "tab-timeline",
Tab::Replay => "tab-replay",
Tab::Corpus => "tab-corpus",
+ Tab::Settings => "tab-settings",
}
}
@@ -94,6 +103,7 @@ impl Tab {
Tab::Timeline => "panel-timeline",
Tab::Replay => "panel-replay",
Tab::Corpus => "panel-corpus",
+ Tab::Settings => "panel-settings",
}
}
@@ -113,6 +123,7 @@ impl Tab {
Self::Timeline => "timeline",
Self::Replay => "replay",
Self::Corpus => "corpus",
+ Self::Settings => "settings",
}
}
@@ -121,6 +132,24 @@ impl Tab {
}
}
+/// Map a persisted [`DefaultTab`] to its runtime [`Tab`] counterpart.
+///
+/// [`DefaultTab`] deliberately omits [`Tab::Settings`] (we never auto-launch
+/// into settings), so every variant maps cleanly.
+fn default_tab_to_tab(t: DefaultTab) -> Tab {
+ match t {
+ DefaultTab::Bundles => Tab::Bundles,
+ DefaultTab::History => Tab::History,
+ DefaultTab::Unfinished => Tab::Unfinished,
+ DefaultTab::Memory => Tab::Memory,
+ DefaultTab::LiveFeed => Tab::LiveFeed,
+ DefaultTab::Search => Tab::Search,
+ DefaultTab::Timeline => Tab::Timeline,
+ DefaultTab::Replay => Tab::Replay,
+ DefaultTab::Corpus => Tab::Corpus,
+ }
+}
+
/// Shared session data provided at the root of the component tree.
///
/// Consumers call `use_context::()` to access the loaded sessions.
@@ -131,6 +160,38 @@ pub struct SessionContext(pub Signal>);
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ReloadTrigger(pub Signal);
+/// User-supplied corpus directories, persisted across launches.
+///
+/// The wrapped signal is mutated by the Raw Sessions tab when the user
+/// picks a new folder or resets to defaults. The discovery effect reads
+/// the latest value on every re-run, so a pick immediately triggers a
+/// reload without restarting the app.
+#[derive(Clone, Debug, PartialEq)]
+pub struct CustomCorpusPaths(pub Signal);
+
+impl CustomCorpusPaths {
+ /// Read the current custom paths snapshot.
+ pub fn snapshot(&self) -> CustomCorpusPath {
+ self.0.cloned()
+ }
+
+ /// Replace the current custom paths (used by the Raw Sessions toolbar).
+ pub fn set(&mut self, paths: CustomCorpusPath) {
+ self.0.set(paths);
+ }
+
+ /// Clear the override and revert to the default discovery set.
+ pub fn clear(&mut self) {
+ self.0.set(CustomCorpusPath::default());
+ }
+}
+
+/// Persisted user settings, exposed at the root so [`SettingsTab`] and
+/// other consumers can read and update them. The settings are persisted
+/// to disk by an effect that watches this signal.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct SettingsSignal(pub Signal);
+
/// Discovery status published at the root so every tab can render a
/// loading / error / ready state without the App's spawn_blocking effect
/// having to thread the status through props. The corpus scan can take
@@ -164,6 +225,22 @@ fn resolve_data_source() -> DataSource {
DataSource::Auto
}
+/// Load the persisted custom corpus paths at startup.
+///
+/// Missing or unreadable files degrade silently to an empty
+/// [`CustomCorpusPath`] — the viewer must always be able to launch, even
+/// if the config directory is locked down. Parse errors are logged to
+/// stderr so the operator notices but the UI does not break.
+fn initial_custom_corpus_paths() -> CustomCorpusPath {
+ match crate::corpus_paths::load_config() {
+ Ok(config) => CustomCorpusPath::from(config.custom_paths),
+ Err(error) => {
+ eprintln!("[sl-viewer] custom corpus paths unavailable: {error}");
+ CustomCorpusPath::default()
+ }
+ }
+}
+
fn initial_tab_for_viewer() -> Tab {
if query_fixture_active("history-empty") {
Tab::History
@@ -174,7 +251,10 @@ fn initial_tab_for_viewer() -> Tab {
} else if query_fixture_active("stream-skeleton") {
Tab::LiveFeed
} else {
- Tab::Bundles
+ // Honour the persisted default tab so a user who lands on Search
+ // every time does not have to click into it on every launch.
+ let persisted = Settings::load();
+ default_tab_to_tab(persisted.default_tab)
}
}
@@ -227,7 +307,6 @@ fn build_bundles_from_sessions(sessions: &[Session]) -> Vec
}
// `App` is a Dioxus component (mounted by name from main.rs / web entry).
-#[allow(non_snake_case)]
/// Inline SVG icons for each tab (loaded at compile time).
const ICON_SVG_BUNDLES: &str = include_str!("../../../assets/icons/line/bundles.svg");
const ICON_SVG_HISTORY: &str = include_str!("../../../assets/icons/line/history.svg");
@@ -238,6 +317,7 @@ const ICON_SVG_LIVE: &str = include_str!("../../../assets/icons/line/live.svg");
const ICON_SVG_SEARCH: &str = include_str!("../../../assets/icons/line/search.svg");
const ICON_SVG_REPLAY: &str = include_str!("../../../assets/icons/line/replay.svg");
const ICON_SVG_CORPUS: &str = include_str!("../../../assets/icons/line/corpus.svg");
+const ICON_SVG_SETTINGS: &str = include_str!("../../../assets/icons/line/settings.svg");
/// Brand mascot (Getta) for the launch splash. Embedded at compile time so
/// the splash renders even before the assets server is reachable. The 2.5D
@@ -258,10 +338,14 @@ fn icon_svg(tab_icon: &str) -> &'static str {
"search" => ICON_SVG_SEARCH,
"replay" => ICON_SVG_REPLAY,
"corpus" => ICON_SVG_CORPUS,
+ "settings" => ICON_SVG_SETTINGS,
_ => ICON_SVG_BUNDLES,
}
}
+// `App` is the Dioxus entry point — main.rs and the web launcher mount it
+// by name, so the upper-case identifier is part of the public surface.
+#[allow(non_snake_case)]
pub fn App() -> Element {
#[cfg(feature = "web")]
use_effect(|| {
@@ -303,27 +387,30 @@ pub fn App() -> Element {
let mut error_signal: Signal