Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
9aafc7c
Merge pull request #157 from anscg/ja4-fingerprinting
anscg Jul 23, 2026
6fd71a2
perf(desktop): cut per-tick CPU/RAM in the Rust capture pipeline
claude Jul 24, 2026
085e595
perf(desktop): make frontend background work event-driven
claude Jul 24, 2026
c9d1f2a
feat(desktop): quality-of-life polish
claude Jul 24, 2026
81086a2
Merge pull request #1 from anscg/claude/lookout-desktop-qol-perf-f8z7o3
anscg Jul 24, 2026
256a25a
Merge branch 'hackclub:main' into main
anscg Jul 24, 2026
37cf1ea
feat(clips): per-minute video clips + configurable desktop server
anscg Jul 25, 2026
d2432ae
feat(desktop): background updates with titlebar pill, instant launch
anscg Jul 25, 2026
87903ae
chore(deploy): add Coolify compose variant without host port binding
anscg Jul 25, 2026
338505c
fix(desktop): allow any https host in the http plugin scope
anscg Jul 25, 2026
d30d39e
fix(server): allow BASE_URL's own origin through CORS
anscg Jul 25, 2026
fa22053
fix(clips): sharper clips, faster compile, dense opening clip
anscg Jul 25, 2026
76d1793
feat(desktop): hardware H.264 clip recording in the capture loop
anscg Jul 25, 2026
f977898
fix(worker): visually-lossless segment encode (CRF 28 -> 18)
anscg Jul 25, 2026
1343bf3
fix(clips): 15 fpm globally, 800kbps budget, pinned encoder GOP
anscg Jul 25, 2026
a2d3980
perf(desktop): upload clips concurrently with frame capture
anscg Jul 25, 2026
808ba0a
fix(ui): label capture units 'captures', not 'screenshots'
anscg Jul 26, 2026
3bbf3ca
feat: native add-menu popup, program icons, session redirect URLs
anscg Jul 26, 2026
ad4fda4
perf(desktop): prefetch add-menu icons into a native cache
anscg Jul 26, 2026
1cd06ad
fix(desktop): anchor update pill bottom-left on Windows/Linux
anscg Jul 26, 2026
c462f96
feat(desktop): DOM add-menu popup on Windows/Linux
anscg Jul 26, 2026
3bacb77
feat: user-editable cuts for compiled timelapses (#edit)
anscg Jul 26, 2026
2cf1195
chore(desktop): rename add-menu entry to 'Start from link'
anscg Jul 26, 2026
05f87a9
feat: move editing into the stop flow, before publication
anscg Jul 26, 2026
d7dfdc7
fix: editor opened into a dead end while the preview was still compiling
anscg Jul 26, 2026
8d2114a
fix(editor): thumbnails silently failed when the preview video lacked…
anscg Jul 26, 2026
e966b16
fix(editor): filmstrip tiles whole frames sized by the track, at devi…
anscg Jul 26, 2026
10aa9a7
feat(editor): the edit hold is a lease the editor renews, not a count…
anscg Jul 26, 2026
b4cd87b
fix(editor): progress ring restarted at 0 on every poll
anscg Jul 26, 2026
9254ccd
feat(editor): drop hover preview, ticked ruler, grabbable playhead tag
anscg Jul 26, 2026
f396a19
fix(editor): ruler labelled time of day, which read as a duration
anscg Jul 26, 2026
fc55288
fix(desktop): let macOS draw the editor window title
anscg Jul 26, 2026
3211d4f
fix(editor): cuts could never stay selected; closing now finishes the…
anscg Jul 26, 2026
1cd6e2a
style(editor): round and hatch the removed-footage states
anscg Jul 26, 2026
716efbd
style(editor): hatch the timeline only, and quieter
anscg Jul 26, 2026
d61d29f
fix(desktop): editor window stayed open after saving, and skipped the…
anscg Jul 26, 2026
5b9ab05
fix(editor): cut intervals assumed a 60s stride, so the server over-c…
anscg Jul 26, 2026
3e89d53
Revert "fix(server): allow BASE_URL's own origin through CORS"
anscg Jul 26, 2026
e7e215d
Revert the session rename animation rework
anscg Jul 26, 2026
02aaa98
feat: SDK playground for exercising the edit flow
anscg Jul 26, 2026
f046ac8
fix(editor): waiting for the preview would 429 on its own poll
anscg Jul 26, 2026
f58b7b9
test: pin that upload time can't cost credited minutes
anscg Jul 26, 2026
e5a9653
fix(clips): web clips encoded at max quantizer, ~20dB below native
anscg Jul 27, 2026
8b3f5c5
fix(compile): the seed capture became a slow-motion first second
anscg Jul 27, 2026
8eba5a1
test: prove the cut is bit-exact, and make the lossy fallback loud
anscg Jul 27, 2026
ffdb0d8
fix(api): /stop must keep ignoring bodies it doesn't recognise
anscg Jul 27, 2026
7d3e9d3
docs: state plainly that embedders get the edit flow for free
anscg Jul 27, 2026
c05deb0
fix(sdk): the embedded editor was narrower than the screen it replaced
anscg Jul 27, 2026
641988e
feat(sdk): the editor opens as a modal overlay, not inline
anscg Jul 27, 2026
136477b
feat(sdk): embedders can replace the accent colour
anscg Jul 27, 2026
48ef2cf
chore: ignore SwiftPM build output and .env backups
Aug 8, 2026
f3289a4
feat(ui): surface real compile progress while a timelapse builds
Aug 8, 2026
5e02ae4
test(server): anchor the edit-hold fixture to now, not a fixed date
Aug 8, 2026
6b97b02
perf(compile): build a throwaway preview, publish from the capture units
Aug 8, 2026
d322f89
feat(clips): 6 frames a minute, on by default
Aug 8, 2026
8a88258
fix(server): a wrong system clock must not cost the recording
Aug 8, 2026
4770f1d
fix(web): a bad network no longer costs minutes of recording
Aug 8, 2026
f802e88
fix(desktop): Windows encoder bugs, and stop retrying a dead encoder
Aug 8, 2026
6e54917
ci: exercise the desktop client in release, and run the shared tests
Aug 8, 2026
dbb624f
0.3.7: clips at 6fpm by default, two-tier compile, network and clock …
Aug 8, 2026
4f8b98b
fix(security): the uncut original must not sit at a guessable key
Aug 9, 2026
faad8da
ci: install the GStreamer plugins the Linux clip encoder needs
Aug 9, 2026
3e6387f
fix(desktop): declare the Linux runtime dependencies
Aug 9, 2026
5658c85
fix(desktop): plug a ~4.9MB-per-clip leak in the macOS encoder
Aug 9, 2026
6f70602
perf(desktop): stop the Filtered Apps page blocking a capture worker
Aug 9, 2026
b86cbc6
test(desktop): fix the flaky temp-file test I added
Aug 9, 2026
14aeb56
test(desktop): assert temp-file cleanup per path, not by scanning the…
Aug 9, 2026
c435d60
Merge pull request #167 from anscg/clips
anscg Aug 9, 2026
1f8d09c
chore(deps): bump actions/setup-node from 6.3.0 to 7.0.0
dependabot[bot] Aug 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ jobs:
zstd

- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: lts/*
cache: npm
Expand Down
101 changes: 97 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: lts/*
cache: npm
Expand All @@ -82,6 +82,47 @@ jobs:
working-directory: packages/server
run: npx vitest run

# ──────────────────────────────────────────────────────────────────
# Worker: ffmpeg segment pipeline. Proves the clip-compile contract —
# every capture unit (JPEG still, VP8 webm clip, H.264 mp4 clip, any
# resolution) normalizes to exactly one second of 30fps video with
# pinned parameters, and the segments stream-copy concatenate into a
# decodable MP4. Runs real ffmpeg on synthetic inputs; no DB/R2.
# ──────────────────────────────────────────────────────────────────
worker:
name: Worker tests
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Install ffmpeg
run: |
sudo apt-get update
sudo apt-get install -y ffmpeg

- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: lts/*
cache: npm

- name: Install dependencies
run: npm ci

- name: Build shared package
run: npm run build -w packages/shared

- name: Type-check worker
working-directory: packages/worker
run: npx tsc --noEmit

- name: Vitest (segment pipeline)
run: npm test -w packages/worker

# Shared package: pure units — cut math, clock-offset estimation.
- name: Vitest (shared)
run: npm test -w packages/shared

# ──────────────────────────────────────────────────────────────────
# Desktop client: Rust unit + serde compat tests.
# The 4-way matrix (legacy/new struct × legacy/new JSON) here is the
Expand Down Expand Up @@ -111,6 +152,25 @@ jobs:
libgstreamer-plugins-base1.0-dev \
libgbm-dev

# RUNTIME GStreamer plugins, not just the dev headers above. The Linux
# clip encoder builds `appsrc ! videoconvert ! x264enc ! h264parse !
# mp4mux ! filesink`, and those elements live in four different packages:
# videoconvert in -base, mp4mux in -good, h264parse in -bad, x264enc in
# -ugly. Without them ClipRecorder::new fails with "no H.264 encoder
# element available" and every clip test that touches a real encoder dies
# — which is exactly how this job has been failing.
#
# This is also the only place the Linux encoder path is executed at all
# (macOS runs it locally, Windows is check-only), so installing them is
# what makes that path tested rather than merely compiled.
- name: Install GStreamer runtime plugins
run: |
sudo apt-get install -y \
gstreamer1.0-plugins-base \
gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad \
gstreamer1.0-plugins-ugly

- name: Install Rust stable
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable

Expand All @@ -126,9 +186,42 @@ jobs:
workspaces: "./clients/desktop/src-tauri -> target"
shared-key: rust-ubuntu-24.04-native

- name: cargo test --lib
# Release, not debug. The clip encoders are unsafe FFI against
# VideoToolbox / Media Foundation / GStreamer, and that is exactly the
# code whose behaviour differs under optimisation — the kind of bug this
# project has already been bitten by once (a datatype-misalignment crash
# that only showed up in a release build). Testing debug binaries proves
# the wrong artifact.
- name: cargo test --lib --release
working-directory: clients/desktop/src-tauri
run: cargo test --lib --release

# ──────────────────────────────────────────────────────────────────
# Windows type-check: the Media Foundation clip encoder is Windows-only
# cfg code that no other job compiles. A check (not full test run)
# catches API drift in the `windows` crate bindings.
# ──────────────────────────────────────────────────────────────────
desktop-rust-windows:
name: Desktop Rust check (Windows)
runs-on: windows-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Install Rust stable
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable

- name: Rust cache
uses: swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: "./clients/desktop/src-tauri -> target"
shared-key: rust-windows-check

# Release profile, matching what actually ships (see the Linux job).
# This is the only job that compiles the Windows-only Media Foundation
# encoder at all, so it should compile it the way users get it.
- name: cargo check --release
working-directory: clients/desktop/src-tauri
run: cargo test --lib
run: cargo check --release

# ──────────────────────────────────────────────────────────────────
# Type-check the rest of the workspace (React/web clients). Catches
Expand All @@ -141,7 +234,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: lts/*
cache: npm
Expand Down
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ dist/
# Rust / Tauri
clients/desktop/src-tauri/target/
clients/desktop/src-tauri/gen/
# SwiftPM build output for the native tray. Its index database is a 64MB file
# that was committed once already; keep it out for good.
clients/desktop/src-tauri/swift/*/.build/

# Env files, including the editor/tooling backups that are easy to commit by
# accident (.env itself is covered above).
.env.bak
.env.*.bak
*.env.bak

# Drizzle generated migrations are committed, but the meta folder is not needed
# (optional — remove this line if you want to commit migration metadata)
Expand Down
5 changes: 3 additions & 2 deletions clients/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lookout/desktop",
"version": "0.3.5",
"version": "0.3.7",
"license": "AGPL-3.0-or-later",
"private": true,
"type": "module",
Expand All @@ -12,7 +12,8 @@
"dependencies": {
"@lookout/react": "*",
"@lookout/shared": "*",
"@number-flow/react": "^0.6.0",
"@number-flow/react": "^0.6.2",
"@phosphor-icons/react": "^2.1.10",
"@sentry/react": "^10.45.0",
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-deep-link": "^2.4.7",
Expand Down
31 changes: 27 additions & 4 deletions clients/desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 31 additions & 3 deletions clients/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ debug = true

[package]
name = "lookout-desktop"
version = "0.3.5"
version = "0.3.7"
edition = "2021"

[lib]
Expand All @@ -21,6 +21,9 @@ crate-type = ["staticlib", "cdylib", "rlib"]

[build-dependencies]
tauri-build = { version = "2", features = [] }
# Compiles swift/lookout-tray (SwiftUI menu-bar item with numericText digit
# animation) and links it into the macOS binary; no-op on other targets.
swift-rs = { version = "1.0.7", features = ["build"] }

[dependencies]
tauri = { version = "2", features = ["macos-private-api", "devtools", "tray-icon", "image-png"] }
Expand All @@ -29,10 +32,17 @@ tauri-plugin-dialog = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
xcap = "0.9"
image = { version = "0.25", default-features = false, features = ["jpeg"] }
image = { version = "0.25", default-features = false, features = ["jpeg", "png"] }
# SIMD resizing (SSE4/AVX2/NEON) for the capture downscale — ~4x faster than
# the `image` crate's scalar filters at screen sizes (29ms → 7ms, 5K→1080p).
fast_image_resize = "5"
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "rustls-tls"] }
tokio = { version = "1", features = ["full"] }
base64 = "0.22"
# Refcounted byte buffers for the upload pipeline — retrying an R2 PUT clones
# the request body, and `Bytes` makes that clone a refcount bump instead of a
# full JPEG copy. Already in the dependency tree via tokio/reqwest.
bytes = "1"
tauri-plugin-deep-link = "2.4.7"
tauri-plugin-http = "2.5.7"
tauri-plugin-macos-permissions = "2.3.0"
Expand All @@ -53,12 +63,30 @@ sentry = "0.38"
tokio = { version = "1", features = ["test-util"] }

[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6"
objc2-core-foundation = "0.3.2"
objc2-core-graphics = "0.3.2"
objc2-foundation = { version = "0.3.2", features = ["NSProcessInfo", "NSString"] }
objc2-foundation = { version = "0.3.2", features = ["NSProcessInfo", "NSString", "NSArray", "NSData", "NSDictionary", "NSEnumerator", "NSBundle", "NSURL"] }
objc2-app-kit = { version = "0.3.2", features = ["NSWorkspace", "NSRunningApplication", "NSImage", "NSImageRep", "NSBitmapImageRep", "NSGraphicsContext", "objc2-core-graphics"] }
# Clip encoding: hardware H.264 via AVAssetWriter (VideoToolbox under the
# hood) writing MP4 directly. All already in the tree transitively via xcap.
objc2-av-foundation = { version = "0.3.2", features = ["AVAssetWriter", "AVAssetWriterInput", "AVMediaFormat", "AVVideoSettings", "objc2-core-media", "objc2-core-video"] }
objc2-core-media = { version = "0.3.2", features = ["CMTime"] }
objc2-core-video = { version = "0.3.2", features = ["CVPixelBuffer", "CVBuffer", "CVReturn", "CVImageBuffer"] }
block2 = "0.6"

[target.'cfg(target_os = "windows")'.dependencies]
webview2-com = "0.38"
windows = { version = "0.62", features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
# Clip encoding: hardware H.264 via the Media Foundation sink writer.
"Win32_Media_MediaFoundation",
"Win32_Storage_FileSystem",
"Win32_System_Com",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
] }

[target."cfg(target_os = \"linux\")".dependencies]
ashpd = { version = "0.9", default-features = false, features = ["tokio"] }
Expand Down
9 changes: 8 additions & 1 deletion clients/desktop/src-tauri/build.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
fn main() {
// Link CoreGraphics on macOS for screen capture permission APIs
#[cfg(target_os = "macos")]
println!("cargo:rustc-link-lib=framework=CoreGraphics");
{
println!("cargo:rustc-link-lib=framework=CoreGraphics");
// Native menu-bar item: compiles swift/lookout-tray (SwiftUI with the
// numericText digit-roll animation) and links it into the binary.
swift_rs::SwiftLinker::new("10.15")
.with_package("lookout-tray", "./swift/lookout-tray")
.link();
}

tauri_build::build()
}
Loading