diff --git a/src-tauri/src/http_server.rs b/src-tauri/src/http_server.rs index 6a7dc7b..5319228 100644 --- a/src-tauri/src/http_server.rs +++ b/src-tauri/src/http_server.rs @@ -1445,6 +1445,20 @@ async fn workspaces(State(state): State) -> Json) -> Result { + let path = format!("/{hash}/"); + let Some(initial_file) = initial_file else { + return Ok(path); + }; + let mut url = + tauri::Url::parse(&format!("http://localhost{path}")).map_err(|error| error.to_string())?; + url.query_pairs_mut().append_pair("file", initial_file); + let query = url + .query() + .ok_or_else(|| "failed to encode browse file query".to_string())?; + Ok(format!("{path}?{query}")) +} + /// `GET /browse?path=` — the landing point for `ide browse`. Ensures a (possibly /// hidden) session exists for `path`, then redirects into its scoped SPA, so a plain /// `open ` from bash is enough to land a browser tab on the right workspace @@ -1465,6 +1479,10 @@ async fn browse(app: tauri::AppHandle, state: HttpServerState, query: BrowseQuer Err(error) => return ApiError::bad_request(error.to_string()).into_response(), }; let hash = workspace_root_hash(&target.workspace_root); + let redirect_path = match browse_redirect_path(&hash, target.initial_file.as_deref()) { + Ok(path) => path, + Err(error) => return ApiError::internal(error).into_response(), + }; let already_open = resolve_workspace_by_hash(&state.window_sessions, &hash) .await @@ -1476,7 +1494,7 @@ async fn browse(app: tauri::AppHandle, state: HttpServerState, query: BrowseQuer } } - Redirect::to(&format!("/{hash}/")).into_response() + Redirect::to(&redirect_path).into_response() } /// Builds the hidden background window for a browse session on the main thread — @@ -2794,6 +2812,15 @@ mod tests { .is_none()); } + #[test] + fn browse_redirect_path_includes_an_encoded_file_target() { + assert_eq!( + browse_redirect_path("abc123", Some("docs/hello world#1.md")).unwrap(), + "/abc123/?file=docs%2Fhello+world%231.md" + ); + assert_eq!(browse_redirect_path("abc123", None).unwrap(), "/abc123/"); + } + #[tokio::test] async fn workspace_summaries_dedupe_by_hash() { let dir = tempdir().unwrap(); diff --git a/src/tauri.test.ts b/src/tauri.test.ts index 4a51395..434d082 100644 --- a/src/tauri.test.ts +++ b/src/tauri.test.ts @@ -16,6 +16,7 @@ describe("hosted Tauri API transport", () => { vi.unstubAllGlobals(); vi.resetModules(); delete (window as Window & { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__; + window.history.replaceState({}, "", "/"); }); it("adds the local bearer token to hosted write requests", async () => { @@ -240,6 +241,26 @@ describe("hosted Tauri API transport", () => { expect(fetchMock.mock.calls[1][0]).toBe("/api/agent-context"); }); + it("reads the initial file from the scoped hosted URL", async () => { + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + window.history.replaceState({}, "", "/abc123/?file=docs%2Fhello+world.md"); + const { getInitialFile } = await import("./tauri"); + + await expect(getInitialFile()).resolves.toBe("docs/hello world.md"); + + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it("normalizes an empty native initial file to undefined", async () => { + (window as Window & { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__ = {}; + const { invoke } = await import("@tauri-apps/api/core"); + vi.mocked(invoke).mockResolvedValueOnce(null); + const { getInitialFile } = await import("./tauri"); + + await expect(getInitialFile()).resolves.toBeUndefined(); + }); + it("does not persist recent files from hosted browser mode", async () => { const fetchMock = vi.fn(); vi.stubGlobal("fetch", fetchMock); diff --git a/src/tauri.ts b/src/tauri.ts index 5589906..bd6a2ff 100644 --- a/src/tauri.ts +++ b/src/tauri.ts @@ -391,8 +391,10 @@ export function getWorkspaceRoot() { } export function getInitialFile() { - if (!isNativeTauri()) return Promise.resolve(undefined); - return invoke("get_initial_file"); + if (!isNativeTauri()) { + return Promise.resolve(new URLSearchParams(window.location.search).get("file") ?? undefined); + } + return invoke("get_initial_file").then((path) => path ?? undefined); } export function getAppInfo() {