Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-resizable-panels": "^2.1.7",
"react-router-dom": "^7.1.3",
"react-syntax-highlighter": "^15.6.1",
"sonner": "^1.7.2",
"tailwind-merge": "^2.6.0",
Expand All @@ -63,7 +62,6 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@shadcn/ui": "^0.0.4",
"@tauri-apps/cli": "^2.2.5",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
Expand Down
421 changes: 0 additions & 421 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

10 changes: 4 additions & 6 deletions src-tauri/src/http_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,11 +244,9 @@ where
{
let mut total = 0.0;
let mut has_value = false;
for value in values {
if let Some(v) = value {
total += v;
has_value = true;
}
for v in values.into_iter().flatten() {
total += v;
has_value = true;
}
has_value.then_some(total)
}
Expand Down Expand Up @@ -366,7 +364,7 @@ fn perform_curl_request(
return true;
};

let line = raw_line.trim_end_matches(|c| c == '\r' || c == '\n');
let line = raw_line.trim_end_matches(['\r', '\n']);
if line.is_empty() {
return true;
}
Expand Down
71 changes: 31 additions & 40 deletions src-tauri/src/oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,25 @@ const AUTH_CALLBACK_TIMEOUT_SECS: u64 = 300;
/// `expires_in` cannot leave a poll loop running for hours.
const DEVICE_CODE_DEFAULT_EXPIRES_SECS: u64 = 900;
const DEVICE_CODE_MAX_EXPIRES_SECS: u64 = 1800;

/// POST a form to an OAuth endpoint with the standard accept header and
/// timeout. Every token/device request in this module sends the same shape;
/// `err_prefix` labels a transport failure ("Token request failed: ...").
async fn post_oauth_form(
client: &reqwest::Client,
url: &str,
params: &HashMap<String, String>,
err_prefix: &str,
) -> Result<reqwest::Response, String> {
client
.post(url)
.header("accept", OAUTH_TOKEN_ACCEPT_HEADER)
.timeout(std::time::Duration::from_secs(30))
.form(params)
.send()
.await
.map_err(|e| format!("{}: {}", err_prefix, e))
}
const DEVICE_POLL_DEFAULT_INTERVAL_SECS: u64 = 5;

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -597,14 +616,7 @@ pub async fn oauth2_token_exchange(
}

let client = client_wrapper.get_or_init_client()?;
let res = client
.post(&token_url)
.header("accept", OAUTH_TOKEN_ACCEPT_HEADER)
.timeout(std::time::Duration::from_secs(30))
.form(&params)
.send()
.await
.map_err(|e| format!("Token request failed: {}", e))?;
let res = post_oauth_form(&client, &token_url, &params, "Token request failed").await?;

if !res.status().is_success() {
return Err(oauth_http_error("Token request failed", res).await);
Expand Down Expand Up @@ -758,14 +770,7 @@ pub async fn oauth2_auth_code_flow(
}

let client = client_wrapper.get_or_init_client()?;
let res = client
.post(&token_url)
.header("accept", OAUTH_TOKEN_ACCEPT_HEADER)
.timeout(std::time::Duration::from_secs(30))
.form(&params)
.send()
.await
.map_err(|e| format!("Token exchange failed: {}", e))?;
let res = post_oauth_form(&client, &token_url, &params, "Token exchange failed").await?;

if !res.status().is_success() {
return Err(oauth_http_error("Token exchange failed", res).await);
Expand Down Expand Up @@ -841,14 +846,13 @@ pub async fn oauth2_device_flow(
params.insert("client_secret".to_string(), secret.clone());
}

let res = client
.post(&device_auth_url)
.header("accept", OAUTH_TOKEN_ACCEPT_HEADER)
.timeout(std::time::Duration::from_secs(30))
.form(&params)
.send()
.await
.map_err(|e| format!("Device authorization request failed: {}", e))?;
let res = post_oauth_form(
&client,
&device_auth_url,
&params,
"Device authorization request failed",
)
.await?;

if !res.status().is_success() {
return Err(oauth_http_error("Device authorization request failed", res).await);
Expand Down Expand Up @@ -914,14 +918,8 @@ pub async fn oauth2_device_flow(
poll_params.insert("client_secret".to_string(), secret.clone());
}

let res = client
.post(&token_url)
.header("accept", OAUTH_TOKEN_ACCEPT_HEADER)
.timeout(std::time::Duration::from_secs(30))
.form(&poll_params)
.send()
.await
.map_err(|e| format!("Token request failed: {}", e))?;
let res =
post_oauth_form(&client, &token_url, &poll_params, "Token request failed").await?;

let status = res.status();
let (content_type, body) = read_response_body(res).await?;
Expand Down Expand Up @@ -1090,14 +1088,7 @@ pub async fn oauth2_refresh(
insert_optional_param(&mut params, "client_secret", options.client_secret);

let client = client_wrapper.get_or_init_client()?;
let res = client
.post(&token_url)
.header("accept", OAUTH_TOKEN_ACCEPT_HEADER)
.timeout(std::time::Duration::from_secs(30))
.form(&params)
.send()
.await
.map_err(|e| format!("Refresh token request failed: {}", e))?;
let res = post_oauth_form(&client, &token_url, &params, "Refresh token request failed").await?;

if !res.status().is_success() {
return Err(oauth_http_error("Refresh failed", res).await);
Expand Down
73 changes: 62 additions & 11 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ function App() {
setActiveTab,
addTab,
closeTab,
closeOtherTabs,
closeTabsToRight,
duplicateTab,
updateTab,
startEditing,
stopEditing,
Expand Down Expand Up @@ -106,6 +109,51 @@ function App() {
return () => window.removeEventListener("keydown", handleKeyDown)
}, [togglePalette])

// Ref mirrors so the tab-shortcut listener stays stable across renders.
const tabsRef = useRef(tabs)
tabsRef.current = tabs
const activeTabRef = useRef(activeTab)
activeTabRef.current = activeTab

// Tab management shortcuts: Ctrl+T new, Ctrl+W close, Ctrl(+Shift)+Tab
// cycle, Ctrl+1–9 jump (9 = last, as in every browser).
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!(e.ctrlKey || e.metaKey)) return
const currentTabs = tabsRef.current
const current = activeTabRef.current

if (e.key === "Tab") {
e.preventDefault()
const index = currentTabs.findIndex((tab) => tab.id === current)
if (index === -1) return
const nextIndex = e.shiftKey
? (index - 1 + currentTabs.length) % currentTabs.length
: (index + 1) % currentTabs.length
setActiveTab(currentTabs[nextIndex].id)
return
}
if (e.shiftKey) return

const key = e.key.toLowerCase()
if (key === "t") {
e.preventDefault()
addTab()
} else if (key === "w") {
e.preventDefault()
closeTab(current)
} else if (e.key >= "1" && e.key <= "9") {
const index = e.key === "9" ? currentTabs.length - 1 : Number(e.key) - 1
if (index < currentTabs.length) {
e.preventDefault()
setActiveTab(currentTabs[index].id)
}
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [addTab, closeTab, setActiveTab])

// Mirror the theme class onto <html> so portaled content (tooltips, menus,
// dialogs) inherits theme tokens instead of falling back to :root defaults.
useEffect(() => {
Expand Down Expand Up @@ -155,24 +203,22 @@ function App() {
const requestUrl = overrides.url ?? tab?.rawUrl
if (!tab || !requestUrl?.trim()) return

const urlUpdates = overrides.url === undefined ? {} : {
rawUrl: overrides.url,
url: overrides.url,
// A user-set tab name survives URL edits.
...(tab.nameEdited ? {} : { name: getRequestNameFromUrl(overrides.url) }),
}
const requestTab = {
...tab,
...(overrides.body === undefined ? {} : { body: overrides.body }),
...(overrides.url === undefined ? {} : {
rawUrl: overrides.url,
url: overrides.url,
name: getRequestNameFromUrl(overrides.url),
}),
...urlUpdates,
}
updateTab(tabId, {
loading: true,
response: null,
...(overrides.body === undefined ? {} : { body: overrides.body }),
...(overrides.url === undefined ? {} : {
rawUrl: overrides.url,
url: overrides.url,
name: getRequestNameFromUrl(overrides.url),
}),
...urlUpdates,
})
const response = await sendRequest(requestTab)
updateTab(tabId, { loading: false, response: response || null })
Expand Down Expand Up @@ -300,6 +346,9 @@ function App() {
onTabChange={setActiveTab}
onAddTab={addTab}
onCloseTab={closeTab}
onCloseOtherTabs={closeOtherTabs}
onCloseTabsToRight={closeTabsToRight}
onDuplicateTab={duplicateTab}
onStartEditing={startEditing}
onStopEditing={stopEditing}
/>
Expand All @@ -318,6 +367,7 @@ function App() {
body={currentTab.body}
contentType={currentTab.contentType}
auth={currentTab.auth}
tabName={currentTab.name}
cookies={currentTab.cookies}
response={currentTab.response}
testScripts={currentTab.testScripts}
Expand All @@ -330,7 +380,8 @@ function App() {
updateTab(currentTab.id, {
rawUrl,
url: rawUrl,
name: getRequestNameFromUrl(rawUrl)
// A user-set tab name survives URL edits.
...(currentTab.nameEdited ? {} : { name: getRequestNameFromUrl(rawUrl) })
})
}}
onParamsChange={(params) => updateTab(currentTab.id, { params })}
Expand Down
3 changes: 2 additions & 1 deletion src/components/CollapsibleJSON.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { ChevronRight, ChevronDown } from "lucide-react"
const CHILDREN_PAGE_SIZE = 100

interface CollapsibleJSONProps {
data: any
/** Any parsed JSON value — object, array, or primitive. */
data: unknown
level?: number
isExpanded?: boolean
maxAutoExpandDepth?: number
Expand Down
Loading
Loading