Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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