Skip to content

Commit b5a3e65

Browse files
committed
feat: support staged: URL scheme to prefill New Project UI
Register a custom `staged:` URL scheme so that navigating to e.g. `staged://github.com/owner/repo/pull/123` in a browser opens Staged and prefills the New Project form with the parsed GitHub URL. Backend: - Add tauri-plugin-deep-link dependency and register the plugin - Configure the `staged` scheme in tauri.conf.json and Info.plist - Add deep-link:default capability permission - Forward incoming URLs to the frontend via a `deep-link-open` event, handling both app-already-running and cold-launch cases Frontend: - Add deepLink.ts utility to convert `staged:` URLs to `https:` URLs - App.svelte listens for `deep-link-open` and dispatches a `staged:new-project-with-url` window event with the converted URL - ProjectsList, NewProjectModal, SplashScreen, and NewProjectForm all accept an optional `initialUrl` prop that gets parsed via the existing `parseGitHubUrl` helper to prefill repo/PR/branch fields
1 parent 8c538a3 commit b5a3e65

12 files changed

Lines changed: 227 additions & 7 deletions

File tree

apps/staged/src-tauri/Cargo.lock

Lines changed: 98 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/staged/src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ tauri-plugin-store = "2.4.2"
5959
# MCP server for project sessions
6060
rmcp = { version = "0.17", features = ["server", "transport-streamable-http-server"] }
6161
axum = { version = "0.8" }
62+
tauri-plugin-deep-link = "2"
6263

6364
# Debug binaries archived — uncomment when needed
6465
# [[bin]]

apps/staged/src-tauri/Info.plist

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,16 @@
66
<string>Staged</string>
77
<key>CFBundleName</key>
88
<string>Staged</string>
9+
<key>CFBundleURLTypes</key>
10+
<array>
11+
<dict>
12+
<key>CFBundleURLName</key>
13+
<string>xyz.block.staged.beta.app</string>
14+
<key>CFBundleURLSchemes</key>
15+
<array>
16+
<string>staged</string>
17+
</array>
18+
</dict>
19+
</array>
920
</dict>
1021
</plist>

apps/staged/src-tauri/capabilities/default.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"dialog:default",
1616
"process:allow-restart",
1717
"updater:default",
18-
"log:default"
18+
"log:default",
19+
"deep-link:default"
1920
]
2021
}

apps/staged/src-tauri/src/lib.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,6 +1049,7 @@ pub fn run() {
10491049
.build(),
10501050
)
10511051
.plugin(tauri_plugin_store::Builder::new().build())
1052+
.plugin(tauri_plugin_deep_link::init())
10521053
.setup(|app| {
10531054
let updater_pubkey_present = app
10541055
.config()
@@ -1273,6 +1274,38 @@ pub fn run() {
12731274
needs_reset: Mutex::new(reset_info),
12741275
});
12751276

1277+
// Deep-link: forward `staged:` URLs to the frontend.
1278+
// `on_open_url` fires when the app is already running and a URL is
1279+
// opened; `get_current` catches the URL that launched the app.
1280+
{
1281+
use tauri_plugin_deep_link::DeepLinkExt;
1282+
1283+
let handle = app.handle().clone();
1284+
app.deep_link().on_open_url(move |event| {
1285+
for url in event.urls() {
1286+
let url_str = url.to_string();
1287+
log::info!("[deep-link] received URL while running: {url_str}");
1288+
let _ = handle.emit("deep-link-open", url_str);
1289+
}
1290+
});
1291+
1292+
// Check if the app was launched via a deep link.
1293+
if let Ok(Some(urls)) = app.deep_link().get_current() {
1294+
let handle = app.handle().clone();
1295+
for url in urls {
1296+
let url_str = url.to_string();
1297+
log::info!("[deep-link] app launched with URL: {url_str}");
1298+
// Emit after a short delay so the frontend has time to
1299+
// mount its listener.
1300+
let h = handle.clone();
1301+
std::thread::spawn(move || {
1302+
std::thread::sleep(std::time::Duration::from_millis(500));
1303+
let _ = h.emit("deep-link-open", url_str);
1304+
});
1305+
}
1306+
}
1307+
}
1308+
12761309
if cfg!(debug_assertions) {
12771310
app.handle().plugin(
12781311
tauri_plugin_log::Builder::default()

apps/staged/src-tauri/tauri.conf.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@
3333
"csp": null
3434
}
3535
},
36+
"plugins": {
37+
"deep-link": {
38+
"desktop": {
39+
"schemes": ["staged"]
40+
}
41+
}
42+
},
3643
"bundle": {
3744
"active": true,
3845
"targets": "all",

apps/staged/src/App.svelte

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import { onMount, onDestroy } from 'svelte';
99
import { getCurrentWindow } from '@tauri-apps/api/window';
1010
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
11+
import { convertDeepLinkToHttps } from './lib/shared/deepLink';
1112
import * as commands from './lib/api/commands';
1213
import TopBar from './lib/features/layout/TopBar.svelte';
1314
import ProjectHome from './lib/features/projects/ProjectHome.svelte';
@@ -45,6 +46,7 @@
4546
const updaterCheckIntervalMs = 15 * 60 * 1000;
4647
4748
let showSessionLab = $state(false);
49+
let unlistenDeepLink: UnlistenFn | undefined;
4850
let unlistenSettings: UnlistenFn | undefined;
4951
let unlistenFind: UnlistenFn | undefined;
5052
let unlistenFindNext: UnlistenFn | undefined;
@@ -190,6 +192,16 @@
190192
onMount(async () => {
191193
document.addEventListener('keydown', handleKonamiKey);
192194
195+
// Listen for deep-link URLs (staged: scheme).
196+
unlistenDeepLink = await listen<string>('deep-link-open', (event) => {
197+
const httpsUrl = convertDeepLinkToHttps(event.payload);
198+
if (httpsUrl) {
199+
window.dispatchEvent(
200+
new CustomEvent('staged:new-project-with-url', { detail: { url: httpsUrl } })
201+
);
202+
}
203+
});
204+
193205
// Listen for the app menu Preferences item.
194206
unlistenSettings = await listen('menu:settings', () => {
195207
if (!triggerShortcut('app-open-settings')) openSettings();
@@ -362,6 +374,7 @@
362374
onDestroy(() => {
363375
document.removeEventListener('keydown', handleKonamiKey);
364376
unregisterShortcuts?.();
377+
unlistenDeepLink?.();
365378
unlistenSettings?.();
366379
unlistenFind?.();
367380
unlistenFindNext?.();

apps/staged/src/lib/features/projects/NewProjectForm.svelte

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
location?: 'local' | 'remote';
3030
selectedRepo?: string | null;
3131
subpath?: string;
32+
initialUrl?: string | null;
3233
}
3334
3435
let {
@@ -38,6 +39,7 @@
3839
location = $bindable('local'),
3940
selectedRepo = $bindable(null),
4041
subpath = $bindable(''),
42+
initialUrl = null,
4143
}: Props = $props();
4244
4345
let branchName = $state('');
@@ -57,6 +59,14 @@
5759
} catch {
5860
// Silently ignore — recents are a convenience, not critical
5961
}
62+
63+
// If opened via a deep link, parse the URL and prefill the form.
64+
if (initialUrl) {
65+
const parsed = parseGitHubUrl(initialUrl);
66+
if (parsed) {
67+
handleRepoSelected(parsed);
68+
}
69+
}
6070
});
6171
6272
async function checkIfMonorepo(repo: string) {

apps/staged/src/lib/features/projects/NewProjectModal.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
interface Props {
1313
onCreated: (project: Project) => void;
1414
onClose: () => void;
15+
initialUrl?: string | null;
1516
}
1617
17-
let { onCreated, onClose }: Props = $props();
18+
let { onCreated, onClose, initialUrl = null }: Props = $props();
1819
const backdropDismiss = createBackdropDismissHandlers({ onDismiss: () => onClose() });
1920
2021
function handleKeydown(e: KeyboardEvent) {
@@ -46,7 +47,7 @@
4647
</div>
4748

4849
<div class="modal-body">
49-
<NewProjectForm {onCreated} onCancel={onClose} />
50+
<NewProjectForm {onCreated} onCancel={onClose} {initialUrl} />
5051
</div>
5152
</div>
5253
</div>

0 commit comments

Comments
 (0)