feat(v2): implement session persistence, file watcher, and markdown viewer
Phase 4 complete (MVP ship): - SessionDb (rusqlite, WAL mode): sessions + layout_state tables, CRUD - FileWatcherManager (notify v6): watch files, emit Tauri change events - MarkdownPane: marked.js rendering with Catppuccin styles, live reload - Layout store wired to persistence (addPane/removePane/setPreset persist) - restoreFromDb() on startup restores panes in layout order - Sidebar "M" button opens file picker for markdown files - New adapters: session-bridge.ts, file-bridge.ts - Deps: rusqlite (bundled), dirs 5, notify 6, marked
This commit is contained in:
parent
5ca035d438
commit
bdb87978a9
14 changed files with 1075 additions and 17 deletions
15
v2/package-lock.json
generated
15
v2/package-lock.json
generated
|
|
@ -11,7 +11,8 @@
|
|||
"@tauri-apps/api": "^2.10.1",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/xterm": "^6.0.0"
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"marked": "^17.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
|
|
@ -1174,6 +1175,18 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "17.0.4",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz",
|
||||
"integrity": "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"@tauri-apps/api": "^2.10.1",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/xterm": "^6.0.0"
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"marked": "^17.0.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
303
v2/src-tauri/Cargo.lock
generated
303
v2/src-tauri/Cargo.lock
generated
|
|
@ -19,6 +19,18 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
|
|
@ -221,8 +233,11 @@ dependencies = [
|
|||
name = "bterminal"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"dirs 5.0.1",
|
||||
"log",
|
||||
"notify",
|
||||
"portable-pty",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
|
|
@ -634,13 +649,34 @@ dependencies = [
|
|||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"dirs-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
"dirs-sys 0.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.4.6",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -651,7 +687,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
|||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"redox_users 0.5.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
|
|
@ -790,6 +826,18 @@ dependencies = [
|
|||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-streaming-iterator"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
|
|
@ -829,6 +877,17 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
|
|
@ -893,6 +952,15 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "2.0.0"
|
||||
|
|
@ -1299,7 +1367,16 @@ version = "0.12.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"ahash 0.7.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1317,6 +1394,15 @@ version = "0.16.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
|
|
@ -1610,6 +1696,26 @@ dependencies = [
|
|||
"cfb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ioctl-rs"
|
||||
version = "0.1.6"
|
||||
|
|
@ -1729,6 +1835,26 @@ dependencies = [
|
|||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue-sys"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kuchikiki"
|
||||
version = "0.8.8-speedreader"
|
||||
|
|
@ -1799,7 +1925,21 @@ version = "0.1.14"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"libc",
|
||||
"plain",
|
||||
"redox_syscall 0.7.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1903,6 +2043,18 @@ dependencies = [
|
|||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.1.1"
|
||||
|
|
@ -1991,6 +2143,25 @@ version = "0.1.14"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "6.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"crossbeam-channel",
|
||||
"filetime",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio 0.8.11",
|
||||
"walkdir",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.0"
|
||||
|
|
@ -2215,7 +2386,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
|||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.5.18",
|
||||
"smallvec",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
|
@ -2378,6 +2549,12 @@ version = "0.3.32"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "plain"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "plist"
|
||||
version = "1.8.0"
|
||||
|
|
@ -2685,6 +2862,26 @@ dependencies = [
|
|||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
"libredox",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.2"
|
||||
|
|
@ -2817,6 +3014,20 @@ dependencies = [
|
|||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
"libsqlite3-sys",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_decimal"
|
||||
version = "1.40.0"
|
||||
|
|
@ -3242,7 +3453,7 @@ dependencies = [
|
|||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
"raw-window-handle",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.5.18",
|
||||
"tracing",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
|
|
@ -3448,7 +3659,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"bytes",
|
||||
"cookie",
|
||||
"dirs",
|
||||
"dirs 6.0.0",
|
||||
"dunce",
|
||||
"embed_plist",
|
||||
"getrandom 0.3.4",
|
||||
|
|
@ -3498,7 +3709,7 @@ checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
"dirs",
|
||||
"dirs 6.0.0",
|
||||
"glob",
|
||||
"heck 0.5.0",
|
||||
"json-patch",
|
||||
|
|
@ -3818,7 +4029,7 @@ checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
|
|||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"mio 1.1.1",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"windows-sys 0.61.2",
|
||||
|
|
@ -4013,7 +4224,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"dirs",
|
||||
"dirs 6.0.0",
|
||||
"libappindicator",
|
||||
"muda",
|
||||
"objc2",
|
||||
|
|
@ -4166,6 +4377,12 @@ version = "1.12.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.1"
|
||||
|
|
@ -4637,6 +4854,15 @@ dependencies = [
|
|||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
|
|
@ -4679,6 +4905,21 @@ dependencies = [
|
|||
"windows_x86_64_msvc 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
|
|
@ -4736,6 +4977,12 @@ version = "0.42.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
|
|
@ -4754,6 +5001,12 @@ version = "0.42.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
|
|
@ -4772,6 +5025,12 @@ version = "0.42.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
|
|
@ -4802,6 +5061,12 @@ version = "0.42.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
|
|
@ -4820,6 +5085,12 @@ version = "0.42.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
|
|
@ -4838,6 +5109,12 @@ version = "0.42.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
|
|
@ -4856,6 +5133,12 @@ version = "0.42.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
|
|
@ -5009,7 +5292,7 @@ dependencies = [
|
|||
"block2",
|
||||
"cookie",
|
||||
"crossbeam-channel",
|
||||
"dirs",
|
||||
"dirs 6.0.0",
|
||||
"dpi",
|
||||
"dunce",
|
||||
"gdkx11",
|
||||
|
|
|
|||
|
|
@ -24,3 +24,6 @@ tauri = { version = "2.10.3", features = [] }
|
|||
tauri-plugin-log = "2"
|
||||
portable-pty = "0.8"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||
dirs = "5"
|
||||
notify = { version = "6", features = ["macos_fsevent"] }
|
||||
|
|
|
|||
|
|
@ -4,13 +4,17 @@ mod watcher;
|
|||
mod session;
|
||||
|
||||
use pty::{PtyManager, PtyOptions};
|
||||
use session::{Session, SessionDb, LayoutState};
|
||||
use sidecar::{AgentQueryOptions, SidecarManager};
|
||||
use watcher::FileWatcherManager;
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
|
||||
struct AppState {
|
||||
pty_manager: Arc<PtyManager>,
|
||||
sidecar_manager: Arc<SidecarManager>,
|
||||
session_db: Arc<SessionDb>,
|
||||
file_watcher: Arc<FileWatcherManager>,
|
||||
}
|
||||
|
||||
// --- PTY commands ---
|
||||
|
|
@ -64,14 +68,90 @@ fn agent_ready(state: State<'_, AppState>) -> bool {
|
|||
state.sidecar_manager.is_ready()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn agent_restart(app: tauri::AppHandle, state: State<'_, AppState>) -> Result<(), String> {
|
||||
state.sidecar_manager.restart(&app)
|
||||
}
|
||||
|
||||
// --- File watcher commands ---
|
||||
|
||||
#[tauri::command]
|
||||
fn file_watch(
|
||||
app: tauri::AppHandle,
|
||||
state: State<'_, AppState>,
|
||||
pane_id: String,
|
||||
path: String,
|
||||
) -> Result<String, String> {
|
||||
state.file_watcher.watch(&app, &pane_id, &path)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn file_unwatch(state: State<'_, AppState>, pane_id: String) {
|
||||
state.file_watcher.unwatch(&pane_id);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn file_read(state: State<'_, AppState>, path: String) -> Result<String, String> {
|
||||
state.file_watcher.read_file(&path)
|
||||
}
|
||||
|
||||
// --- Session persistence commands ---
|
||||
|
||||
#[tauri::command]
|
||||
fn session_list(state: State<'_, AppState>) -> Result<Vec<Session>, String> {
|
||||
state.session_db.list_sessions()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn session_save(state: State<'_, AppState>, session: Session) -> Result<(), String> {
|
||||
state.session_db.save_session(&session)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn session_delete(state: State<'_, AppState>, id: String) -> Result<(), String> {
|
||||
state.session_db.delete_session(&id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn session_update_title(state: State<'_, AppState>, id: String, title: String) -> Result<(), String> {
|
||||
state.session_db.update_title(&id, &title)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn session_touch(state: State<'_, AppState>, id: String) -> Result<(), String> {
|
||||
state.session_db.touch_session(&id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn layout_save(state: State<'_, AppState>, layout: LayoutState) -> Result<(), String> {
|
||||
state.session_db.save_layout(&layout)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn layout_load(state: State<'_, AppState>) -> Result<LayoutState, String> {
|
||||
state.session_db.load_layout()
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let pty_manager = Arc::new(PtyManager::new());
|
||||
let sidecar_manager = Arc::new(SidecarManager::new());
|
||||
|
||||
// Initialize session database in app data directory
|
||||
let data_dir = dirs::data_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join("bterminal");
|
||||
let session_db = Arc::new(
|
||||
SessionDb::open(&data_dir).expect("Failed to open session database")
|
||||
);
|
||||
|
||||
let file_watcher = Arc::new(FileWatcherManager::new());
|
||||
|
||||
let app_state = AppState {
|
||||
pty_manager,
|
||||
sidecar_manager: sidecar_manager.clone(),
|
||||
session_db,
|
||||
file_watcher,
|
||||
};
|
||||
|
||||
tauri::Builder::default()
|
||||
|
|
@ -84,6 +164,17 @@ pub fn run() {
|
|||
agent_query,
|
||||
agent_stop,
|
||||
agent_ready,
|
||||
agent_restart,
|
||||
file_watch,
|
||||
file_unwatch,
|
||||
file_read,
|
||||
session_list,
|
||||
session_save,
|
||||
session_delete,
|
||||
session_update_title,
|
||||
session_touch,
|
||||
layout_save,
|
||||
layout_load,
|
||||
])
|
||||
.setup(move |app| {
|
||||
if cfg!(debug_assertions) {
|
||||
|
|
|
|||
|
|
@ -1,2 +1,175 @@
|
|||
// Session persistence via rusqlite
|
||||
// Phase 4: CRUD, layout save/restore
|
||||
// Stores sessions, layout preferences, and last-used state
|
||||
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Session {
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub session_type: String,
|
||||
pub title: String,
|
||||
pub shell: Option<String>,
|
||||
pub cwd: Option<String>,
|
||||
pub args: Option<Vec<String>>,
|
||||
pub created_at: i64,
|
||||
pub last_used_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LayoutState {
|
||||
pub preset: String,
|
||||
pub pane_ids: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct SessionDb {
|
||||
conn: Mutex<Connection>,
|
||||
}
|
||||
|
||||
impl SessionDb {
|
||||
pub fn open(data_dir: &PathBuf) -> Result<Self, String> {
|
||||
std::fs::create_dir_all(data_dir)
|
||||
.map_err(|e| format!("Failed to create data dir: {e}"))?;
|
||||
|
||||
let db_path = data_dir.join("sessions.db");
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open database: {e}"))?;
|
||||
|
||||
// Enable WAL mode for better concurrent read performance
|
||||
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")
|
||||
.map_err(|e| format!("Failed to set pragmas: {e}"))?;
|
||||
|
||||
let db = Self { conn: Mutex::new(conn) };
|
||||
db.migrate()?;
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
fn migrate(&self) -> Result<(), String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
shell TEXT,
|
||||
cwd TEXT,
|
||||
args TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
last_used_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS layout_state (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
preset TEXT NOT NULL DEFAULT '1-col',
|
||||
pane_ids TEXT NOT NULL DEFAULT '[]'
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO layout_state (id, preset, pane_ids) VALUES (1, '1-col', '[]');
|
||||
"
|
||||
).map_err(|e| format!("Migration failed: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_sessions(&self) -> Result<Vec<Session>, String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT id, type, title, shell, cwd, args, created_at, last_used_at FROM sessions ORDER BY last_used_at DESC")
|
||||
.map_err(|e| format!("Query prepare failed: {e}"))?;
|
||||
|
||||
let sessions = stmt
|
||||
.query_map([], |row| {
|
||||
let args_json: Option<String> = row.get(5)?;
|
||||
let args: Option<Vec<String>> = args_json.and_then(|j| serde_json::from_str(&j).ok());
|
||||
Ok(Session {
|
||||
id: row.get(0)?,
|
||||
session_type: row.get(1)?,
|
||||
title: row.get(2)?,
|
||||
shell: row.get(3)?,
|
||||
cwd: row.get(4)?,
|
||||
args,
|
||||
created_at: row.get(6)?,
|
||||
last_used_at: row.get(7)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| format!("Query failed: {e}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("Row read failed: {e}"))?;
|
||||
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
pub fn save_session(&self, session: &Session) -> Result<(), String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let args_json = session.args.as_ref().map(|a| serde_json::to_string(a).unwrap_or_default());
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO sessions (id, type, title, shell, cwd, args, created_at, last_used_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||
params![
|
||||
session.id,
|
||||
session.session_type,
|
||||
session.title,
|
||||
session.shell,
|
||||
session.cwd,
|
||||
args_json,
|
||||
session.created_at,
|
||||
session.last_used_at,
|
||||
],
|
||||
).map_err(|e| format!("Insert failed: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_session(&self, id: &str) -> Result<(), String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute("DELETE FROM sessions WHERE id = ?1", params![id])
|
||||
.map_err(|e| format!("Delete failed: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_title(&self, id: &str, title: &str) -> Result<(), String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"UPDATE sessions SET title = ?1 WHERE id = ?2",
|
||||
params![title, id],
|
||||
).map_err(|e| format!("Update failed: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn touch_session(&self, id: &str) -> Result<(), String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64;
|
||||
conn.execute(
|
||||
"UPDATE sessions SET last_used_at = ?1 WHERE id = ?2",
|
||||
params![now, id],
|
||||
).map_err(|e| format!("Touch failed: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save_layout(&self, layout: &LayoutState) -> Result<(), String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let pane_ids_json = serde_json::to_string(&layout.pane_ids).unwrap_or_default();
|
||||
conn.execute(
|
||||
"UPDATE layout_state SET preset = ?1, pane_ids = ?2 WHERE id = 1",
|
||||
params![layout.preset, pane_ids_json],
|
||||
).map_err(|e| format!("Layout save failed: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_layout(&self) -> Result<LayoutState, String> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT preset, pane_ids FROM layout_state WHERE id = 1")
|
||||
.map_err(|e| format!("Layout query failed: {e}"))?;
|
||||
|
||||
stmt.query_row([], |row| {
|
||||
let preset: String = row.get(0)?;
|
||||
let pane_ids_json: String = row.get(1)?;
|
||||
let pane_ids: Vec<String> = serde_json::from_str(&pane_ids_json).unwrap_or_default();
|
||||
Ok(LayoutState { preset, pane_ids })
|
||||
}).map_err(|e| format!("Layout read failed: {e}"))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,97 @@
|
|||
// File watcher for markdown viewer
|
||||
// Phase 4: notify crate, debounce, Tauri events
|
||||
// Uses notify crate to watch files and emit Tauri events on change
|
||||
|
||||
use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use tauri::Emitter;
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
struct FileChangedPayload {
|
||||
pane_id: String,
|
||||
path: String,
|
||||
content: String,
|
||||
}
|
||||
|
||||
struct WatchEntry {
|
||||
_watcher: RecommendedWatcher,
|
||||
_path: PathBuf,
|
||||
}
|
||||
|
||||
pub struct FileWatcherManager {
|
||||
watchers: Mutex<HashMap<String, WatchEntry>>,
|
||||
}
|
||||
|
||||
impl FileWatcherManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
watchers: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn watch(
|
||||
&self,
|
||||
app: &tauri::AppHandle,
|
||||
pane_id: &str,
|
||||
path: &str,
|
||||
) -> Result<String, String> {
|
||||
let file_path = PathBuf::from(path);
|
||||
if !file_path.exists() {
|
||||
return Err(format!("File not found: {path}"));
|
||||
}
|
||||
|
||||
// Read initial content
|
||||
let content = std::fs::read_to_string(&file_path)
|
||||
.map_err(|e| format!("Failed to read file: {e}"))?;
|
||||
|
||||
// Set up watcher
|
||||
let app_handle = app.clone();
|
||||
let pane_id_owned = pane_id.to_string();
|
||||
let watch_path = file_path.clone();
|
||||
|
||||
let mut watcher = RecommendedWatcher::new(
|
||||
move |res: Result<Event, notify::Error>| {
|
||||
if let Ok(event) = res {
|
||||
if event.kind.is_modify() {
|
||||
if let Ok(new_content) = std::fs::read_to_string(&watch_path) {
|
||||
let _ = app_handle.emit(
|
||||
"file-changed",
|
||||
FileChangedPayload {
|
||||
pane_id: pane_id_owned.clone(),
|
||||
path: watch_path.to_string_lossy().to_string(),
|
||||
content: new_content,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Config::default(),
|
||||
)
|
||||
.map_err(|e| format!("Failed to create watcher: {e}"))?;
|
||||
|
||||
watcher
|
||||
.watch(file_path.parent().unwrap_or(&file_path), RecursiveMode::NonRecursive)
|
||||
.map_err(|e| format!("Failed to watch path: {e}"))?;
|
||||
|
||||
let mut watchers = self.watchers.lock().unwrap();
|
||||
watchers.insert(pane_id.to_string(), WatchEntry {
|
||||
_watcher: watcher,
|
||||
_path: file_path,
|
||||
});
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
pub fn unwatch(&self, pane_id: &str) {
|
||||
let mut watchers = self.watchers.lock().unwrap();
|
||||
watchers.remove(pane_id);
|
||||
}
|
||||
|
||||
pub fn read_file(&self, path: &str) -> Result<String, String> {
|
||||
std::fs::read_to_string(path)
|
||||
.map_err(|e| format!("Failed to read file: {e}"))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { onMount, onDestroy } from 'svelte';
|
||||
import SessionList from './lib/components/Sidebar/SessionList.svelte';
|
||||
import TilingGrid from './lib/components/Layout/TilingGrid.svelte';
|
||||
import { addPane, focusPaneByIndex, getPanes } from './lib/stores/layout.svelte';
|
||||
import { addPane, focusPaneByIndex, getPanes, restoreFromDb } from './lib/stores/layout.svelte';
|
||||
import { startAgentDispatcher, stopAgentDispatcher } from './lib/agent-dispatcher';
|
||||
|
||||
function newTerminal() {
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
|
||||
onMount(() => {
|
||||
startAgentDispatcher();
|
||||
restoreFromDb();
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
// Ctrl+N — new terminal
|
||||
|
|
|
|||
29
v2/src/lib/adapters/file-bridge.ts
Normal file
29
v2/src/lib/adapters/file-bridge.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||
|
||||
export interface FileChangedPayload {
|
||||
pane_id: string;
|
||||
path: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
/** Start watching a file; returns initial content */
|
||||
export async function watchFile(paneId: string, path: string): Promise<string> {
|
||||
return invoke('file_watch', { paneId, path });
|
||||
}
|
||||
|
||||
export async function unwatchFile(paneId: string): Promise<void> {
|
||||
return invoke('file_unwatch', { paneId });
|
||||
}
|
||||
|
||||
export async function readFile(path: string): Promise<string> {
|
||||
return invoke('file_read', { path });
|
||||
}
|
||||
|
||||
export async function onFileChanged(
|
||||
callback: (payload: FileChangedPayload) => void
|
||||
): Promise<UnlistenFn> {
|
||||
return listen<FileChangedPayload>('file-changed', (event) => {
|
||||
callback(event.payload);
|
||||
});
|
||||
}
|
||||
45
v2/src/lib/adapters/session-bridge.ts
Normal file
45
v2/src/lib/adapters/session-bridge.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
export interface PersistedSession {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
shell?: string;
|
||||
cwd?: string;
|
||||
args?: string[];
|
||||
created_at: number;
|
||||
last_used_at: number;
|
||||
}
|
||||
|
||||
export interface PersistedLayout {
|
||||
preset: string;
|
||||
pane_ids: string[];
|
||||
}
|
||||
|
||||
export async function listSessions(): Promise<PersistedSession[]> {
|
||||
return invoke('session_list');
|
||||
}
|
||||
|
||||
export async function saveSession(session: PersistedSession): Promise<void> {
|
||||
return invoke('session_save', { session });
|
||||
}
|
||||
|
||||
export async function deleteSession(id: string): Promise<void> {
|
||||
return invoke('session_delete', { id });
|
||||
}
|
||||
|
||||
export async function updateSessionTitle(id: string, title: string): Promise<void> {
|
||||
return invoke('session_update_title', { id, title });
|
||||
}
|
||||
|
||||
export async function touchSession(id: string): Promise<void> {
|
||||
return invoke('session_touch', { id });
|
||||
}
|
||||
|
||||
export async function saveLayout(layout: PersistedLayout): Promise<void> {
|
||||
return invoke('layout_save', { layout });
|
||||
}
|
||||
|
||||
export async function loadLayout(): Promise<PersistedLayout> {
|
||||
return invoke('layout_load');
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
import PaneContainer from './PaneContainer.svelte';
|
||||
import TerminalPane from '../Terminal/TerminalPane.svelte';
|
||||
import AgentPane from '../Agent/AgentPane.svelte';
|
||||
import MarkdownPane from '../Markdown/MarkdownPane.svelte';
|
||||
import {
|
||||
getPanes,
|
||||
getGridTemplate,
|
||||
|
|
@ -54,9 +55,15 @@
|
|||
cwd={pane.cwd}
|
||||
onExit={() => removePane(pane.id)}
|
||||
/>
|
||||
{:else if pane.type === 'markdown'}
|
||||
<MarkdownPane
|
||||
paneId={pane.id}
|
||||
filePath={pane.cwd ?? ''}
|
||||
onExit={() => removePane(pane.id)}
|
||||
/>
|
||||
{:else}
|
||||
<div class="placeholder">
|
||||
<p>{pane.type} pane — coming in Phase 4</p>
|
||||
<p>{pane.type} pane — coming soon</p>
|
||||
</div>
|
||||
{/if}
|
||||
</PaneContainer>
|
||||
|
|
|
|||
196
v2/src/lib/components/Markdown/MarkdownPane.svelte
Normal file
196
v2/src/lib/components/Markdown/MarkdownPane.svelte
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { marked } from 'marked';
|
||||
import { watchFile, unwatchFile, onFileChanged, type FileChangedPayload } from '../../adapters/file-bridge';
|
||||
|
||||
interface Props {
|
||||
filePath: string;
|
||||
paneId: string;
|
||||
onExit?: () => void;
|
||||
}
|
||||
|
||||
let { filePath, paneId, onExit }: Props = $props();
|
||||
|
||||
let renderedHtml = $state('');
|
||||
let error = $state('');
|
||||
let unlisten: (() => void) | undefined;
|
||||
|
||||
function renderMarkdown(source: string): void {
|
||||
try {
|
||||
renderedHtml = marked.parse(source, { async: false }) as string;
|
||||
error = '';
|
||||
} catch (e) {
|
||||
error = `Render error: ${e}`;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const content = await watchFile(paneId, filePath);
|
||||
renderMarkdown(content);
|
||||
|
||||
unlisten = await onFileChanged((payload: FileChangedPayload) => {
|
||||
if (payload.pane_id === paneId) {
|
||||
renderMarkdown(payload.content);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
error = `Failed to open file: ${e}`;
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unlisten?.();
|
||||
unwatchFile(paneId).catch(() => {});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="markdown-pane">
|
||||
{#if error}
|
||||
<div class="error">{error}</div>
|
||||
{:else}
|
||||
<div class="markdown-body">
|
||||
{@html renderedHtml}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="file-path">{filePath}</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.markdown-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px 20px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.markdown-body :global(h1) {
|
||||
font-size: 1.6em;
|
||||
font-weight: 700;
|
||||
margin: 0.8em 0 0.4em;
|
||||
color: var(--ctp-lavender);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.markdown-body :global(h2) {
|
||||
font-size: 1.3em;
|
||||
font-weight: 600;
|
||||
margin: 0.7em 0 0.3em;
|
||||
color: var(--ctp-blue);
|
||||
}
|
||||
|
||||
.markdown-body :global(h3) {
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
margin: 0.6em 0 0.3em;
|
||||
color: var(--ctp-sapphire);
|
||||
}
|
||||
|
||||
.markdown-body :global(p) {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.markdown-body :global(code) {
|
||||
background: var(--bg-surface);
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9em;
|
||||
color: var(--ctp-green);
|
||||
}
|
||||
|
||||
.markdown-body :global(pre) {
|
||||
background: var(--bg-surface);
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--border-radius);
|
||||
overflow-x: auto;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
margin: 0.6em 0;
|
||||
}
|
||||
|
||||
.markdown-body :global(pre code) {
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.markdown-body :global(blockquote) {
|
||||
border-left: 3px solid var(--ctp-mauve);
|
||||
margin: 0.5em 0;
|
||||
padding: 4px 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.markdown-body :global(ul), .markdown-body :global(ol) {
|
||||
padding-left: 24px;
|
||||
margin: 0.4em 0;
|
||||
}
|
||||
|
||||
.markdown-body :global(li) {
|
||||
margin: 0.2em 0;
|
||||
}
|
||||
|
||||
.markdown-body :global(a) {
|
||||
color: var(--ctp-blue);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-body :global(a:hover) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown-body :global(table) {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 0.5em 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.markdown-body :global(th), .markdown-body :global(td) {
|
||||
border: 1px solid var(--border);
|
||||
padding: 6px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-body :global(th) {
|
||||
background: var(--bg-surface);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-body :global(hr) {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown-body :global(img) {
|
||||
max-width: 100%;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.file-path {
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 4px 12px;
|
||||
font-size: 10px;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--ctp-red);
|
||||
padding: 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -33,15 +33,46 @@
|
|||
title: `Agent ${num}`,
|
||||
});
|
||||
}
|
||||
|
||||
let fileInputEl: HTMLInputElement | undefined = $state();
|
||||
|
||||
function openMarkdown() {
|
||||
fileInputEl?.click();
|
||||
}
|
||||
|
||||
function handleFileSelect(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
// Tauri file paths from input elements include the full path
|
||||
const path = (file as any).path ?? file.name;
|
||||
const id = crypto.randomUUID();
|
||||
addPane({
|
||||
id,
|
||||
type: 'markdown',
|
||||
title: file.name,
|
||||
cwd: path,
|
||||
});
|
||||
input.value = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="session-list">
|
||||
<div class="header">
|
||||
<h2>Sessions</h2>
|
||||
<div class="header-buttons">
|
||||
<button class="new-btn" onclick={openMarkdown} title="Open markdown file">M</button>
|
||||
<button class="new-btn" onclick={newAgent} title="New agent (Ctrl+Shift+N)">A</button>
|
||||
<button class="new-btn" onclick={newTerminal} title="New terminal (Ctrl+N)">+</button>
|
||||
</div>
|
||||
<input
|
||||
bind:this={fileInputEl}
|
||||
type="file"
|
||||
accept=".md,.markdown,.txt"
|
||||
onchange={handleFileSelect}
|
||||
style="display: none;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="layout-presets">
|
||||
|
|
@ -65,7 +96,7 @@
|
|||
{#each panes as pane (pane.id)}
|
||||
<li class="pane-item" class:focused={pane.focused}>
|
||||
<button class="pane-btn" onclick={() => focusPane(pane.id)}>
|
||||
<span class="pane-icon">{pane.type === 'terminal' ? '>' : pane.type === 'agent' ? '*' : '#'}</span>
|
||||
<span class="pane-icon">{pane.type === 'terminal' ? '>' : pane.type === 'agent' ? '*' : pane.type === 'markdown' ? 'M' : '#'}</span>
|
||||
<span class="pane-name">{pane.title}</span>
|
||||
</button>
|
||||
<button class="remove-btn" onclick={() => removePane(pane.id)}>×</button>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,14 @@
|
|||
import {
|
||||
listSessions,
|
||||
saveSession,
|
||||
deleteSession,
|
||||
updateSessionTitle,
|
||||
touchSession,
|
||||
saveLayout,
|
||||
loadLayout,
|
||||
type PersistedSession,
|
||||
} from '../adapters/session-bridge';
|
||||
|
||||
export type LayoutPreset = '1-col' | '2-col' | '3-col' | '2x2' | 'master-stack';
|
||||
|
||||
export type PaneType = 'terminal' | 'agent' | 'markdown' | 'empty';
|
||||
|
|
@ -15,6 +26,33 @@ export interface Pane {
|
|||
let panes = $state<Pane[]>([]);
|
||||
let activePreset = $state<LayoutPreset>('1-col');
|
||||
let focusedPaneId = $state<string | null>(null);
|
||||
let initialized = false;
|
||||
|
||||
// --- Persistence helpers (fire-and-forget with error logging) ---
|
||||
|
||||
function persistSession(pane: Pane): void {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const session: PersistedSession = {
|
||||
id: pane.id,
|
||||
type: pane.type,
|
||||
title: pane.title,
|
||||
shell: pane.shell,
|
||||
cwd: pane.cwd,
|
||||
args: pane.args,
|
||||
created_at: now,
|
||||
last_used_at: now,
|
||||
};
|
||||
saveSession(session).catch(e => console.warn('Failed to persist session:', e));
|
||||
}
|
||||
|
||||
function persistLayout(): void {
|
||||
saveLayout({
|
||||
preset: activePreset,
|
||||
pane_ids: panes.map(p => p.id),
|
||||
}).catch(e => console.warn('Failed to persist layout:', e));
|
||||
}
|
||||
|
||||
// --- Public API ---
|
||||
|
||||
export function getPanes(): Pane[] {
|
||||
return panes;
|
||||
|
|
@ -32,6 +70,8 @@ export function addPane(pane: Omit<Pane, 'focused'>): void {
|
|||
panes.push({ ...pane, focused: false });
|
||||
focusPane(pane.id);
|
||||
autoPreset();
|
||||
persistSession({ ...pane, focused: false });
|
||||
persistLayout();
|
||||
}
|
||||
|
||||
export function removePane(id: string): void {
|
||||
|
|
@ -40,11 +80,14 @@ export function removePane(id: string): void {
|
|||
focusedPaneId = panes.length > 0 ? panes[0].id : null;
|
||||
}
|
||||
autoPreset();
|
||||
deleteSession(id).catch(e => console.warn('Failed to delete session:', e));
|
||||
persistLayout();
|
||||
}
|
||||
|
||||
export function focusPane(id: string): void {
|
||||
focusedPaneId = id;
|
||||
panes = panes.map(p => ({ ...p, focused: p.id === id }));
|
||||
touchSession(id).catch(e => console.warn('Failed to touch session:', e));
|
||||
}
|
||||
|
||||
export function focusPaneByIndex(index: number): void {
|
||||
|
|
@ -55,6 +98,53 @@ export function focusPaneByIndex(index: number): void {
|
|||
|
||||
export function setPreset(preset: LayoutPreset): void {
|
||||
activePreset = preset;
|
||||
persistLayout();
|
||||
}
|
||||
|
||||
export function renamePaneTitle(id: string, title: string): void {
|
||||
const pane = panes.find(p => p.id === id);
|
||||
if (pane) {
|
||||
pane.title = title;
|
||||
updateSessionTitle(id, title).catch(e => console.warn('Failed to update title:', e));
|
||||
}
|
||||
}
|
||||
|
||||
/** Restore panes and layout from SQLite on app startup */
|
||||
export async function restoreFromDb(): Promise<void> {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
|
||||
try {
|
||||
const [sessions, layout] = await Promise.all([listSessions(), loadLayout()]);
|
||||
|
||||
if (layout.preset) {
|
||||
activePreset = layout.preset as LayoutPreset;
|
||||
}
|
||||
|
||||
// Restore panes in layout order, falling back to DB order
|
||||
const sessionMap = new Map(sessions.map(s => [s.id, s]));
|
||||
const orderedIds = layout.pane_ids.length > 0 ? layout.pane_ids : sessions.map(s => s.id);
|
||||
|
||||
for (const id of orderedIds) {
|
||||
const s = sessionMap.get(id);
|
||||
if (!s) continue;
|
||||
panes.push({
|
||||
id: s.id,
|
||||
type: s.type as PaneType,
|
||||
title: s.title,
|
||||
shell: s.shell ?? undefined,
|
||||
cwd: s.cwd ?? undefined,
|
||||
args: s.args ?? undefined,
|
||||
focused: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (panes.length > 0) {
|
||||
focusPane(panes[0].id);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to restore sessions from DB:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function autoPreset(): void {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue