From 5a5f9f2d961cf9ab2ddc4196ee793de03e899673 Mon Sep 17 00:00:00 2001 From: Shambhu Lal Verma Date: Sun, 13 Sep 2026 14:15:39 +0530 Subject: [PATCH 1/8] fix: request editor keeps stale folder config after a move Moving a request (or an ancestor folder) via the sidebar left any open request-editor panel pinned to the folderId it was opened with, so it kept using the old collection's base URL, headers, and environment instead of the destination's. Track the panel's current folder separately and push a refresh whenever it moves. Co-Authored-By: Claude Sonnet 5 --- src/providers/RequestEditorProvider.ts | 99 ++++++++++++++++++++------ src/providers/SidebarProvider.ts | 8 +++ src/webview/request/RequestContext.tsx | 4 ++ 3 files changed, 90 insertions(+), 21 deletions(-) diff --git a/src/providers/RequestEditorProvider.ts b/src/providers/RequestEditorProvider.ts index 13366f1..854a97c 100644 --- a/src/providers/RequestEditorProvider.ts +++ b/src/providers/RequestEditorProvider.ts @@ -46,6 +46,12 @@ export class RequestEditorProvider { // Track open panels by request ID private static openPanels: Map = new Map(); + // Tracks the folder a request's open panel currently belongs to, kept in + // sync when the request (or an ancestor folder) is moved elsewhere. The + // panel's message handlers read from this map instead of the folderId + // that was captured when the panel was first opened. + private static panelFolderIds: Map = new Map(); + constructor(private readonly context: vscode.ExtensionContext) {} // Update panel title for an open request editor @@ -74,6 +80,38 @@ export class RequestEditorProvider { }); } + /** + * Called when a request (or an ancestor folder) is moved to a new + * location. Updates the folder this panel's handlers resolve inherited + * config against, and — if the panel is open — pushes the new folder's + * inherited config/environment to the webview without touching the + * in-progress (possibly unsaved) request fields. + */ + public static notifyRequestMoved( + requestId: string, + newFolderId: string, + sidebarProvider: SidebarProvider, + ): void { + RequestEditorProvider.panelFolderIds.set(requestId, newFolderId); + + const panel = RequestEditorProvider.openPanels.get(requestId); + if (!panel) return; + + const folderConfig = sidebarProvider.getInheritedConfig(newFolderId); + const envVariables = sidebarProvider.getActiveEnvVariables(newFolderId); + const collectionData = sidebarProvider.getCollectionData(newFolderId); + const collectionId = sidebarProvider.getRootCollectionId(newFolderId); + + panel.webview.postMessage({ + type: "folderConfigUpdated", + folderConfig, + envVariables, + environments: collectionData.environments, + activeEnvironmentId: collectionData.activeEnvironmentId, + collectionId, + }); + } + /** Push a fresh configLoaded payload to a single open panel, if it exists. Used after a global-history restore, which has no open editor form of its own to update. */ /** Push a fresh historyUpdated payload to a single open panel, if it exists. Used after a global-history delete/clear affecting that request. */ public static refreshPanelHistory(requestId: string, historyManager: HistoryManager): void { @@ -174,6 +212,12 @@ export class RequestEditorProvider { // Store the panel reference RequestEditorProvider.openPanels.set(requestId, panel); + RequestEditorProvider.panelFolderIds.set(requestId, folderId); + + // Resolves the request's current folder, kept up to date across moves + // (see notifyRequestMoved) instead of the folderId captured above. + const getFolderId = () => + RequestEditorProvider.panelFolderIds.get(requestId) ?? folderId; // Notify sidebar of the initially active panel sidebarProvider?.notifyActiveRequest(requestId); @@ -181,6 +225,7 @@ export class RequestEditorProvider { // Remove from map when panel is closed panel.onDidDispose(() => { RequestEditorProvider.openPanels.delete(requestId); + RequestEditorProvider.panelFolderIds.delete(requestId); sidebarProvider?.notifyActiveRequest(null); }); @@ -191,27 +236,33 @@ export class RequestEditorProvider { } if (e.webviewPanel.visible) { // Send updated folder config to webview + const currentFolderId = getFolderId(); const folderConfig = sidebarProvider - ? sidebarProvider.getInheritedConfig(folderId) + ? sidebarProvider.getInheritedConfig(currentFolderId) : context.globalState.get<{ baseUrl?: string; headers?: { key: string; value: string }[]; - }>(`restlab.folder.${folderId}`) || {}; + }>(`restlab.folder.${currentFolderId}`) || {}; const envVariables = sidebarProvider - ? sidebarProvider.getActiveEnvVariables(folderId) + ? sidebarProvider.getActiveEnvVariables(currentFolderId) : {}; const collectionData = sidebarProvider - ? sidebarProvider.getCollectionData(folderId) + ? sidebarProvider.getCollectionData(currentFolderId) : { environments: [], activeEnvironmentId: null }; + const collectionId = sidebarProvider + ? sidebarProvider.getRootCollectionId(currentFolderId) + : currentFolderId; + panel.webview.postMessage({ type: "folderConfigUpdated", folderConfig: folderConfig, envVariables: envVariables, environments: collectionData.environments, activeEnvironmentId: collectionData.activeEnvironmentId, + collectionId, }); } }); @@ -227,31 +278,32 @@ export class RequestEditorProvider { // Handle messages from webview panel.webview.onDidReceiveMessage(async (message) => { switch (message.type) { - case "getConfig": + case "getConfig": { // Always read fresh config from globalState to get latest folder settings + const currentFolderId = getFolderId(); const savedRequest = context.globalState.get( `restlab.request.${requestId}`, ); // Get inherited config from sidebar provider (walks up parent chain) const folderConfig = sidebarProvider - ? sidebarProvider.getInheritedConfig(folderId) + ? sidebarProvider.getInheritedConfig(currentFolderId) : context.globalState.get<{ baseUrl?: string; headers?: { key: string; value: string }[]; - }>(`restlab.folder.${folderId}`) || {}; + }>(`restlab.folder.${currentFolderId}`) || {}; // Get active environment variables const envVariables = sidebarProvider - ? sidebarProvider.getActiveEnvVariables(folderId) + ? sidebarProvider.getActiveEnvVariables(currentFolderId) : {}; const collectionId = sidebarProvider - ? sidebarProvider.getRootCollectionId(folderId) - : folderId; + ? sidebarProvider.getRootCollectionId(currentFolderId) + : currentFolderId; const collectionData = sidebarProvider - ? sidebarProvider.getCollectionData(folderId) + ? sidebarProvider.getCollectionData(currentFolderId) : { environments: [], activeEnvironmentId: null }; panel.webview.postMessage({ @@ -259,7 +311,7 @@ export class RequestEditorProvider { config: { id: requestId, name: requestName, - folderId, + folderId: currentFolderId, method: savedRequest?.method || "GET", url: savedRequest?.url || "", headers: savedRequest?.headers || [], @@ -278,15 +330,17 @@ export class RequestEditorProvider { history: historyManager.getForRequest(requestId), }); break; - case "saveConfig": + } + case "saveConfig": { await context.globalState.update( `restlab.request.${requestId}`, message.config, ); + const currentFolderId = getFolderId(); // Update method in sidebar if it changed if (sidebarProvider && message.config.method) { sidebarProvider.updateRequestMethod( - folderId, + currentFolderId, requestId, message.config.method, ); @@ -294,7 +348,7 @@ export class RequestEditorProvider { // Update name in sidebar if it changed if (sidebarProvider && message.config.name) { sidebarProvider.updateRequestName( - folderId, + currentFolderId, requestId, message.config.name, ); @@ -302,15 +356,17 @@ export class RequestEditorProvider { panel.title = message.config.name; } break; - case "setActiveEnvironment": + } + case "setActiveEnvironment": { if (sidebarProvider) { + const currentFolderId = getFolderId(); await sidebarProvider.setCollectionActiveEnvironment( - folderId, + currentFolderId, message.envId ?? null, ); - const newEnvVars = sidebarProvider.getActiveEnvVariables(folderId); - const newCollData = sidebarProvider.getCollectionData(folderId); - const rootId = sidebarProvider.getRootCollectionId(folderId); + const newEnvVars = sidebarProvider.getActiveEnvVariables(currentFolderId); + const newCollData = sidebarProvider.getCollectionData(currentFolderId); + const rootId = sidebarProvider.getRootCollectionId(currentFolderId); RequestEditorProvider.broadcastToAllPanels({ type: "environmentUpdated", collectionId: rootId, @@ -320,6 +376,7 @@ export class RequestEditorProvider { }); } break; + } case "sendRequest": { const recordHistory = async (response: ResponseData) => { const snapshot = message.historySnapshot || {}; @@ -332,7 +389,7 @@ export class RequestEditorProvider { await historyManager.addEntry({ requestId, requestName, - folderId, + folderId: getFolderId(), request: { method: snapshot.method || message.method, url: snapshot.url || "", diff --git a/src/providers/SidebarProvider.ts b/src/providers/SidebarProvider.ts index 07ab1b5..02a5630 100644 --- a/src/providers/SidebarProvider.ts +++ b/src/providers/SidebarProvider.ts @@ -754,6 +754,11 @@ export class SidebarProvider implements vscode.WebviewViewProvider { ); } + // If this request is open in an editor panel, refresh it with the + // target folder's inherited config instead of leaving it pointed at + // the folder it was moved out of. + RequestEditorProvider.notifyRequestMoved(requestId, targetFolderId, this); + this._saveFolders(); this._sendFoldersToWebview(); vscode.window.showInformationMessage( @@ -882,6 +887,9 @@ export class SidebarProvider implements vscode.WebviewViewProvider { requestConfig, ); } + // Refresh any open editor panel for this request with the new + // (possibly new-collection) inherited config. + RequestEditorProvider.notifyRequestMoved(request.id, folder.id, this); } } diff --git a/src/webview/request/RequestContext.tsx b/src/webview/request/RequestContext.tsx index 080dc89..cfca41d 100644 --- a/src/webview/request/RequestContext.tsx +++ b/src/webview/request/RequestContext.tsx @@ -318,6 +318,10 @@ export const RequestContextProvider: React.FC = ({ if (message.activeEnvironmentId !== undefined) { setActiveEnvironmentId(message.activeEnvironmentId); } + if (message.collectionId !== undefined) { + setCollectionId(message.collectionId); + collectionIdRef.current = message.collectionId; + } break; case "environmentUpdated": if ( From 1a655504e5547bf42485012de0ff16ccf742025b Mon Sep 17 00:00:00 2001 From: Shambhu Lal Verma Date: Sun, 13 Sep 2026 14:16:18 +0530 Subject: [PATCH 2/8] fix: clearer active-environment state and default-active on create - The "set active" toggle was a plain circle that only changed fill color (read as a radio button, gave no feedback in the list itself). Replace it with an icon-only tick that's colored when active and only hinted on hover otherwise, plus an explicit "Active" badge in the list row. - The first environment created for a collection is now made active automatically; later ones leave the current active environment alone. - Restructure the variables table into the same two-column (checkbox+key | value+delete) row layout used elsewhere, collapsing to one column on a narrow panel via a container query. Co-Authored-By: Claude Sonnet 5 --- src/webview/components/icons/CheckIcon.tsx | 17 +++++ src/webview/editor/EnvironmentsTab.tsx | 75 ++++++++++++---------- src/webview/editor/FolderEditor.tsx | 9 ++- 3 files changed, 67 insertions(+), 34 deletions(-) create mode 100644 src/webview/components/icons/CheckIcon.tsx diff --git a/src/webview/components/icons/CheckIcon.tsx b/src/webview/components/icons/CheckIcon.tsx new file mode 100644 index 0000000..d2d294f --- /dev/null +++ b/src/webview/components/icons/CheckIcon.tsx @@ -0,0 +1,17 @@ +import React from "react"; +const CheckIcon = () => ( + + + +); + +export default CheckIcon; diff --git a/src/webview/editor/EnvironmentsTab.tsx b/src/webview/editor/EnvironmentsTab.tsx index 9bc113e..be7e836 100644 --- a/src/webview/editor/EnvironmentsTab.tsx +++ b/src/webview/editor/EnvironmentsTab.tsx @@ -1,6 +1,7 @@ import React, { useRef } from "react"; import Tooltip from "../components/Tooltip"; import AutoGrowTextarea from "../components/AutoGrowTextarea"; +import CheckIcon from "../components/icons/CheckIcon"; import PlusIcon from "../components/icons/PlusIcon"; import TrashIcon from "../components/icons/TrashIcon"; import { Environment, FolderConfig } from "./types"; @@ -72,7 +73,7 @@ const EnvironmentsTab: React.FC = ({ {envs.length > 0 && (

- ● = active  ·  double-click name to rename + ✓ = active  ·  double-click name to rename

)} {envs.length === 0 ? ( @@ -101,7 +102,9 @@ const EnvironmentsTab: React.FC = ({ e.stopPropagation(); onSetActive(env.id); }} - /> + > + + {isRenaming ? ( = ({ {env.name} )} + {isActive && !isRenaming && ( + Active + )} - + + + + ))} diff --git a/src/webview/editor/FolderEditor.tsx b/src/webview/editor/FolderEditor.tsx index 9e92604..0edd3bd 100644 --- a/src/webview/editor/FolderEditor.tsx +++ b/src/webview/editor/FolderEditor.tsx @@ -159,12 +159,19 @@ export const FolderEditor: React.FC = ({ }; const handleAddEnvironment = () => { + const isFirstEnvironment = getEnvs().length === 0; const newEnv: Environment = { id: `env-${Date.now()}`, name: `Environment ${getEnvs().length + 1}`, variables: [], }; - setConfig((prev) => ({ ...prev, environments: [...(prev.environments || []), newEnv] })); + setConfig((prev) => ({ + ...prev, + environments: [...(prev.environments || []), newEnv], + activeEnvironmentId: isFirstEnvironment + ? newEnv.id + : prev.activeEnvironmentId, + })); setSelectedEnvId(newEnv.id); mark(); }; From ae2314f57a097f0bbb86311dd8c35a1c350fb476 Mon Sep 17 00:00:00 2001 From: Shambhu Lal Verma Date: Sun, 13 Sep 2026 14:16:28 +0530 Subject: [PATCH 3/8] fix: Collection/Folder settings row layout and popup clipping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Headers/Query Parameters rows in the settings editor were cramped on a narrow panel: the key/value fields (and their checkbox/delete button) shrank together instead of ever wrapping, and a plain sibling next to a wrapped AutocompleteInput/EnvVarInput never sized evenly with it since the wrapper div, not the class on the inner textarea, is the actual flex item. Restructure each row into two explicit column-groups (checkbox+name | value+delete) that collapse to a single column via a container query. - The `{{variable}}` completion popup rendered inside a fieldset with `position: relative`, which creates its own stacking context — a later sibling fieldset (e.g. "Headers") painted over the popup regardless of its z-index. Portal it to with a live computed position instead, matching how AutocompleteInput already avoids this. Co-Authored-By: Claude Sonnet 5 --- src/webview/editor/EnvVarInput.tsx | 61 ++++++++----- src/webview/editor/SettingsTab.tsx | 132 ++++++++++++++++------------- src/webview/editor/styles.css | 129 +++++++++++++++++----------- 3 files changed, 193 insertions(+), 129 deletions(-) diff --git a/src/webview/editor/EnvVarInput.tsx b/src/webview/editor/EnvVarInput.tsx index 385f2a2..79d0834 100644 --- a/src/webview/editor/EnvVarInput.tsx +++ b/src/webview/editor/EnvVarInput.tsx @@ -1,4 +1,5 @@ import React, { useRef } from "react"; +import ReactDOM from "react-dom"; interface EnvVarInputProps { value: string; @@ -23,7 +24,9 @@ const EnvVarInput: React.FC = ({ const [showPopup, setShowPopup] = React.useState(false); const [filterText, setFilterText] = React.useState(""); const [activeIdx, setActiveIdx] = React.useState(0); + const [popupStyle, setPopupStyle] = React.useState({}); const inputRef = useRef(null); + const containerRef = useRef(null); React.useEffect(() => { const el = inputRef.current; @@ -58,6 +61,23 @@ const EnvVarInput: React.FC = ({ } }; + // Position the popup via a portal anchored to the input's live screen + // position instead of a locally `position: absolute` element. A locally + // absolute popup sits inside this input's fieldset stacking context, so a + // later sibling fieldset (e.g. "Headers") paints over it regardless of + // z-index — portaling to with `position: fixed` escapes that. + React.useEffect(() => { + if (showPopup && containerRef.current) { + const rect = containerRef.current.getBoundingClientRect(); + setPopupStyle({ + position: "fixed", + top: rect.bottom + 4, + left: rect.left, + minWidth: Math.max(rect.width, 280), + }); + } + }, [showPopup]); + const getFiltered = () => varKeys.filter((k) => k.toLowerCase().includes(filterText.toLowerCase())); @@ -102,7 +122,7 @@ const EnvVarInput: React.FC = ({ const filtered = getFiltered(); return ( -
+