From 3e3a9e0f29aed6497c03a3fb7a2878e3c05bbff0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 06:32:25 +0000 Subject: [PATCH 1/6] Initial plan From 0fe222c9f7d938ae1e3408aacbc02de59f88a9bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 06:37:51 +0000 Subject: [PATCH 2/6] Add Arc context awareness for commands and queries Agent-Logs-Url: https://github.com/Cratis/Lens/sessions/04c942fe-6efe-4d76-9b16-f847802ce2c1 Co-authored-by: einari <134365+einari@users.noreply.github.com> --- .../GettingStarted/LocalDevelopment/index.md | 14 +- Source/manifest.json | 1 + Source/src/options/Options.tsx | 33 ++- Source/src/options/components/ArcSettings.tsx | 24 ++- .../src/options/components/CommandsPanel.tsx | 36 ++-- .../src/options/components/QueriesPanel.tsx | 38 ++-- Source/src/options/options.css | 12 ++ Source/src/popup/Popup.tsx | 7 + Source/src/shared/arc-context.ts | 196 ++++++++++++++++++ 9 files changed, 314 insertions(+), 47 deletions(-) create mode 100644 Source/src/shared/arc-context.ts diff --git a/Documentation/GettingStarted/LocalDevelopment/index.md b/Documentation/GettingStarted/LocalDevelopment/index.md index ad3c101..cf19a29 100644 --- a/Documentation/GettingStarted/LocalDevelopment/index.md +++ b/Documentation/GettingStarted/LocalDevelopment/index.md @@ -39,14 +39,15 @@ This generates `Source/dist/`, including `manifest.json`. The **Lens - Cratis Developer Tools** extension should now appear in your extension list. -## 4. Set up Arc connection +## 4. Open Lens on an Arc application page -1. Open the extension options page from the extension details. -2. Set **Arc Base URL** to your local Arc host, for example `http://localhost:5000`. -3. Keep the default **Tenant Header Name** unless your app expects a different header. -4. Select **Save Settings**. +1. Navigate to your Arc application in Chrome. +2. Open the Lens extension popup once on that page so Lens can detect Arc context. +3. Open the extension options page from the extension details. +4. Keep the default **Tenant Header Name** unless your app expects a different header. +5. Select **Save Settings**. -Lens can now fetch command and query introspection metadata from your Arc application. +Lens now uses the detected Arc context and current page location for command and query introspection. ## 5. Use watch mode while developing @@ -57,4 +58,3 @@ npm run dev ``` Vite rebuilds the extension when you change source files. Reload the extension in `chrome://extensions` after each rebuild. - diff --git a/Source/manifest.json b/Source/manifest.json index 7947467..d459ed3 100644 --- a/Source/manifest.json +++ b/Source/manifest.json @@ -5,6 +5,7 @@ "description": "Browser extension bringing Cratis insights and developer productivity tools to your fingertips.", "permissions": [ "storage", + "scripting", "declarativeNetRequest", "declarativeNetRequestWithHostAccess" ], diff --git a/Source/src/options/Options.tsx b/Source/src/options/Options.tsx index cfa0c6a..71f5243 100644 --- a/Source/src/options/Options.tsx +++ b/Source/src/options/Options.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; import { ExtensionSettings } from '../shared/types'; import { getSettings, saveSettings } from '../shared/storage'; +import { ArcContextSnapshot, getArcContextSnapshot } from '../shared/arc-context'; import { UserList } from './components/UserList'; import { TenantList } from './components/TenantList'; import { ArcSettings } from './components/ArcSettings'; @@ -12,13 +13,25 @@ type Tab = 'users' | 'tenants' | 'arc' | 'commands' | 'queries'; export function Options() { const [settings, setSettings] = useState(null); + const [arcContext, setArcContext] = useState(null); + const [arcContextLoading, setArcContextLoading] = useState(true); const [activeTab, setActiveTab] = useState('users'); const [saved, setSaved] = useState(false); useEffect(() => { getSettings().then(setSettings); + getArcContextSnapshot() + .then(setArcContext) + .finally(() => setArcContextLoading(false)); }, []); + useEffect(() => { + const arcAvailable = arcContext?.isArcApplication === true; + if (!arcAvailable && (activeTab === 'commands' || activeTab === 'queries')) { + setActiveTab('arc'); + } + }, [arcContext, activeTab]); + const handleChange = async (updated: ExtensionSettings) => { setSettings(updated); await saveSettings(updated); @@ -30,13 +43,18 @@ export function Options() { return
Loading settings…
; } + const hasArcContext = arcContext?.isArcApplication === true; + const arcBaseUrl = arcContext?.baseUrl ?? ''; + const tabs: { id: Tab; label: string }[] = [ { id: 'users', label: 'Users' }, { id: 'tenants', label: 'Tenants' }, { id: 'arc', label: 'Arc Settings' }, - { id: 'commands', label: 'Commands' }, - { id: 'queries', label: 'Queries' }, ]; + if (hasArcContext) { + tabs.push({ id: 'commands', label: 'Commands' }); + tabs.push({ id: 'queries', label: 'Queries' }); + } return (
@@ -58,6 +76,11 @@ export function Options() {
+ {!arcContextLoading && !hasArcContext && ( +
+ This is not an Arc application. Open Lens on an Arc application page to enable Commands and Queries. +
+ )} {activeTab === 'users' && ( )} @@ -65,13 +88,13 @@ export function Options() { )} {activeTab === 'arc' && ( - + )} {activeTab === 'commands' && ( - + )} {activeTab === 'queries' && ( - + )}
diff --git a/Source/src/options/components/ArcSettings.tsx b/Source/src/options/components/ArcSettings.tsx index 292fb78..fd3c5a8 100644 --- a/Source/src/options/components/ArcSettings.tsx +++ b/Source/src/options/components/ArcSettings.tsx @@ -1,12 +1,14 @@ import { useState } from 'react'; import { ExtensionSettings } from '../../shared/types'; +import { ArcContextSnapshot } from '../../shared/arc-context'; interface Props { settings: ExtensionSettings; onChange: (settings: ExtensionSettings) => void; + arcContext: ArcContextSnapshot | null; } -export function ArcSettings({ settings, onChange }: Props) { +export function ArcSettings({ settings, onChange, arcContext }: Props) { const [arcBaseUrl, setArcBaseUrl] = useState(settings.arcBaseUrl); const [tenantHeaderName, setTenantHeaderName] = useState(settings.tenantHeaderName); @@ -22,9 +24,23 @@ export function ArcSettings({ settings, onChange }: Props) {
Connection
+ {arcContext?.isArcApplication && arcContext.baseUrl ? ( + <> +

+ Commands and queries use Arc context detected from the current page. +

+
+ {arcContext.baseUrl} +
+ + ) : ( +

+ Arc context is unavailable for the current page. Commands and Queries are hidden until Lens is opened on an Arc application page. +

+ )} -
- +
+

- The base URL of your Arc application. Used to fetch introspection data and to scope header injection. + Optional scope used for request header injection rules.

diff --git a/Source/src/options/components/CommandsPanel.tsx b/Source/src/options/components/CommandsPanel.tsx index 2891ea0..4f9144f 100644 --- a/Source/src/options/components/CommandsPanel.tsx +++ b/Source/src/options/components/CommandsPanel.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { CommandIntrospectionMetadata } from '../../shared/types'; interface Props { @@ -19,17 +19,22 @@ interface CommandState { } export function CommandsPanel({ arcBaseUrl }: Props) { - const [baseUrl, setBaseUrl] = useState(arcBaseUrl); const [commands, setCommands] = useState(null); const [fetchError, setFetchError] = useState(null); const [fetching, setFetching] = useState(false); const [states, setStates] = useState>({}); const fetchCommands = useCallback(async () => { + if (!arcBaseUrl) { + setCommands(null); + setFetchError('Arc base URL unavailable. Open Lens on an Arc application page.'); + return; + } + setFetching(true); setFetchError(null); try { - const url = `${baseUrl.replace(/\/$/, '')}/.cratis/commands`; + const url = `${arcBaseUrl.replace(/\/$/, '')}/.cratis/commands`; const res = await fetch(url); if (!res.ok) { setFetchError(`HTTP ${res.status}: ${res.statusText}`); @@ -47,7 +52,11 @@ export function CommandsPanel({ arcBaseUrl }: Props) { } finally { setFetching(false); } - }, [baseUrl]); + }, [arcBaseUrl]); + + useEffect(() => { + void fetchCommands(); + }, [fetchCommands]); const toggleExpanded = (type: string) => { setStates(prev => ({ @@ -63,7 +72,7 @@ export function CommandsPanel({ arcBaseUrl }: Props) { const invoke = async (cmd: CommandIntrospectionMetadata) => { setStates(prev => ({ ...prev, [cmd.type]: { ...prev[cmd.type], loading: true, result: null } })); try { - const url = `${baseUrl.replace(/\/$/, '')}${cmd.route}`; + const url = `${arcBaseUrl.replace(/\/$/, '')}${cmd.route}`; const state = states[cmd.type]; let bodyData: BodyInit | null = null; try { @@ -104,14 +113,11 @@ export function CommandsPanel({ arcBaseUrl }: Props) {
- setBaseUrl(e.target.value)} - placeholder="http://localhost:5000" - /> -
{fetchError && ( @@ -123,13 +129,13 @@ export function CommandsPanel({ arcBaseUrl }: Props) { {commands === null && !fetchError && (
-

Enter your Arc base URL and click "Fetch Commands" to discover available commands.

+

Loading commands from Arc introspection…

)} {commands !== null && commands.length === 0 && (
-

No commands discovered from {baseUrl}/.cratis/commands.

+

No commands discovered from {arcBaseUrl}/.cratis/commands.

)} diff --git a/Source/src/options/components/QueriesPanel.tsx b/Source/src/options/components/QueriesPanel.tsx index d937a10..0c695f1 100644 --- a/Source/src/options/components/QueriesPanel.tsx +++ b/Source/src/options/components/QueriesPanel.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { QueryIntrospectionMetadata } from '../../shared/types'; interface Props { @@ -32,17 +32,22 @@ function buildUrl(baseUrl: string, route: string, params: Record } export function QueriesPanel({ arcBaseUrl }: Props) { - const [baseUrl, setBaseUrl] = useState(arcBaseUrl); const [queries, setQueries] = useState(null); const [fetchError, setFetchError] = useState(null); const [fetching, setFetching] = useState(false); const [states, setStates] = useState>({}); const fetchQueries = useCallback(async () => { + if (!arcBaseUrl) { + setQueries(null); + setFetchError('Arc base URL unavailable. Open Lens on an Arc application page.'); + return; + } + setFetching(true); setFetchError(null); try { - const url = `${baseUrl.replace(/\/$/, '')}/.cratis/queries`; + const url = `${arcBaseUrl.replace(/\/$/, '')}/.cratis/queries`; const res = await fetch(url); if (!res.ok) { setFetchError(`HTTP ${res.status}: ${res.statusText}`); @@ -63,7 +68,11 @@ export function QueriesPanel({ arcBaseUrl }: Props) { } finally { setFetching(false); } - }, [baseUrl]); + }, [arcBaseUrl]); + + useEffect(() => { + void fetchQueries(); + }, [fetchQueries]); const toggleExpanded = (type: string) => { setStates(prev => ({ @@ -83,7 +92,7 @@ export function QueriesPanel({ arcBaseUrl }: Props) { setStates(prev => ({ ...prev, [query.type]: { ...prev[query.type], loading: true, result: null } })); try { const state = states[query.type]; - const url = buildUrl(baseUrl, query.route, state.params); + const url = buildUrl(arcBaseUrl, query.route, state.params); const res = await fetch(url, { method: 'GET' }); let body = ''; const contentType = res.headers.get('content-type') ?? ''; @@ -112,14 +121,11 @@ export function QueriesPanel({ arcBaseUrl }: Props) {
- setBaseUrl(e.target.value)} - placeholder="http://localhost:5000" - /> -
{fetchError && ( @@ -131,13 +137,13 @@ export function QueriesPanel({ arcBaseUrl }: Props) { {queries === null && !fetchError && (
-

Enter your Arc base URL and click "Fetch Queries" to discover available queries.

+

Loading queries from Arc introspection…

)} {queries !== null && queries.length === 0 && (
-

No queries discovered from {baseUrl}/.cratis/queries.

+

No queries discovered from {arcBaseUrl}/.cratis/queries.

)} @@ -146,7 +152,7 @@ export function QueriesPanel({ arcBaseUrl }: Props) { {queries.map(query => { const state = states[query.type] ?? { expanded: false, params: {}, result: null, loading: false }; const paramNames = extractPathParams(query.route); - const finalUrl = buildUrl(baseUrl, query.route, state.params); + const finalUrl = buildUrl(arcBaseUrl, query.route, state.params); return (
diff --git a/Source/src/options/options.css b/Source/src/options/options.css index 049e33e..39c1969 100644 --- a/Source/src/options/options.css +++ b/Source/src/options/options.css @@ -119,6 +119,16 @@ body { min-height: 400px; } +.warning-banner { + background: rgba(249, 226, 175, 0.08); + border: 1px solid var(--accent-yellow); + border-radius: var(--radius-sm); + color: var(--accent-yellow); + font-size: 13px; + padding: 10px 12px; + margin-bottom: 16px; +} + /* Cards */ .card { background: var(--bg-card); @@ -382,6 +392,8 @@ select { /* Arc panel */ .arc-url-row { display: flex; + align-items: center; + justify-content: space-between; gap: 8px; margin-bottom: 16px; } diff --git a/Source/src/popup/Popup.tsx b/Source/src/popup/Popup.tsx index 6d85a13..2670c01 100644 --- a/Source/src/popup/Popup.tsx +++ b/Source/src/popup/Popup.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { ExtensionSettings, UserProfile, Tenant } from '../shared/types'; import { getSettings, saveSettings, getActiveUser, getActiveTenant } from '../shared/storage'; +import { captureArcContextForActiveTab, saveArcContextSnapshot } from '../shared/arc-context'; import './popup.css'; export function Popup() { @@ -10,6 +11,12 @@ export function Popup() { getSettings().then(setSettings); }, []); + useEffect(() => { + captureArcContextForActiveTab() + .then(saveArcContextSnapshot) + .catch(() => undefined); + }, []); + const selectUser = useCallback(async (userId: string) => { if (!settings) return; const updated = { ...settings, activeUserId: userId }; diff --git a/Source/src/shared/arc-context.ts b/Source/src/shared/arc-context.ts new file mode 100644 index 0000000..f1a078c --- /dev/null +++ b/Source/src/shared/arc-context.ts @@ -0,0 +1,196 @@ +export interface ArcContextSnapshot { + isArcApplication: boolean; + baseUrl: string | null; + pageOrigin: string | null; + configuration: Record | null; + capturedAt: number; +} + +const ARC_CONTEXT_SNAPSHOT_KEY = 'arcContextSnapshot'; + +interface ArcContextDetectionResult { + isArcApplication: boolean; + baseUrl: string | null; + pageOrigin: string; + configuration: Record | null; +} + +function getOrigin(url: string | undefined): string | null { + if (!url) return null; + try { + return new URL(url).origin; + } catch { + return null; + } +} + +function toSnapshot(result: ArcContextDetectionResult): ArcContextSnapshot { + return { + isArcApplication: result.isArcApplication, + baseUrl: result.baseUrl, + pageOrigin: result.pageOrigin, + configuration: result.configuration, + capturedAt: Date.now(), + }; +} + +function createNonArcSnapshot(pageOrigin: string | null): ArcContextSnapshot { + return { + isArcApplication: false, + baseUrl: null, + pageOrigin, + configuration: null, + capturedAt: Date.now(), + }; +} + +function detectArcContextFromPage(): ArcContextDetectionResult { + function sanitize(value: unknown, depth = 0): unknown { + if (depth > 6) return null; + if (value === null || value === undefined) return value; + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value; + if (Array.isArray(value)) return value.map(item => sanitize(item, depth + 1)); + if (typeof value === 'object') { + const source = value as Record; + const output: Record = {}; + for (const [key, child] of Object.entries(source)) { + if (typeof child === 'function') continue; + output[key] = sanitize(child, depth + 1); + } + return output; + } + return null; + } + + function toAbsoluteBaseUrl(value: unknown, pageOrigin: string): string | null { + if (typeof value !== 'string' || !value.trim()) return null; + try { + return new URL(value, pageOrigin).origin; + } catch { + return null; + } + } + + function getConfiguredBaseUrl(configuration: Record | null, pageOrigin: string): string | null { + if (!configuration) return null; + const candidates = [ + configuration.baseUrl, + configuration.apiBaseUrl, + configuration.apiSurface, + configuration.apiSurfaceBaseUrl, + configuration.baseUri, + configuration.baseAddress, + ]; + + for (const candidate of candidates) { + const resolved = toAbsoluteBaseUrl(candidate, pageOrigin); + if (resolved) { + return resolved; + } + } + + return null; + } + + const pageOrigin = window.location.origin; + const root = document.getElementById('root'); + if (!root) { + return { + isArcApplication: false, + baseUrl: null, + pageOrigin, + configuration: null, + }; + } + + const fiberKey = Object.keys(root).find(key => key.startsWith('__reactFiber')); + if (!fiberKey) { + return { + isArcApplication: false, + baseUrl: null, + pageOrigin, + configuration: null, + }; + } + + const visited = new Set(); + const queue: unknown[] = [(root as unknown as Record)[fiberKey]]; + + while (queue.length > 0) { + const node = queue.shift() as Record | undefined; + if (!node || visited.has(node)) continue; + visited.add(node); + + const memoizedProps = node.memoizedProps as Record | undefined; + const contextValue = memoizedProps?.value; + + if ( + contextValue && + typeof contextValue === 'object' && + 'reconnectQueries' in (contextValue as Record) + ) { + const context = contextValue as Record; + const configuration = sanitize(context.configuration) as Record | null; + const baseUrl = getConfiguredBaseUrl(configuration, pageOrigin) ?? pageOrigin; + + return { + isArcApplication: true, + baseUrl, + pageOrigin, + configuration, + }; + } + + const child = node.child; + const sibling = node.sibling; + const parent = node.return; + if (child && !visited.has(child)) queue.push(child); + if (sibling && !visited.has(sibling)) queue.push(sibling); + if (parent && !visited.has(parent)) queue.push(parent); + } + + return { + isArcApplication: false, + baseUrl: null, + pageOrigin, + configuration: null, + }; +} + +export async function captureArcContextForActiveTab(): Promise { + const [activeTab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true }); + const pageOrigin = getOrigin(activeTab?.url); + if (!activeTab?.id || !activeTab.url) { + return createNonArcSnapshot(pageOrigin); + } + + if (activeTab.url.startsWith('chrome://') || activeTab.url.startsWith('chrome-extension://')) { + return createNonArcSnapshot(pageOrigin); + } + + try { + const [executionResult] = await chrome.scripting.executeScript({ + target: { tabId: activeTab.id }, + world: 'MAIN', + func: detectArcContextFromPage, + }); + + const result = executionResult?.result as ArcContextDetectionResult | undefined; + if (!result) { + return createNonArcSnapshot(pageOrigin); + } + + return toSnapshot(result); + } catch { + return createNonArcSnapshot(pageOrigin); + } +} + +export async function saveArcContextSnapshot(snapshot: ArcContextSnapshot): Promise { + await chrome.storage.session.set({ [ARC_CONTEXT_SNAPSHOT_KEY]: snapshot }); +} + +export async function getArcContextSnapshot(): Promise { + const data = await chrome.storage.session.get(ARC_CONTEXT_SNAPSHOT_KEY); + return (data[ARC_CONTEXT_SNAPSHOT_KEY] as ArcContextSnapshot | undefined) ?? null; +} From 75dcfaf40a18027d965e2ebe28107cdbb0c02f38 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 06:39:11 +0000 Subject: [PATCH 3/6] Refine Arc context traversal constants and typing Agent-Logs-Url: https://github.com/Cratis/Lens/sessions/04c942fe-6efe-4d76-9b16-f847802ce2c1 Co-authored-by: einari <134365+einari@users.noreply.github.com> --- Source/src/shared/arc-context.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Source/src/shared/arc-context.ts b/Source/src/shared/arc-context.ts index f1a078c..3c6bd56 100644 --- a/Source/src/shared/arc-context.ts +++ b/Source/src/shared/arc-context.ts @@ -15,6 +15,9 @@ interface ArcContextDetectionResult { configuration: Record | null; } +const MAX_SANITIZATION_DEPTH = 6; +const ARC_CONTEXT_MARKER_PROPERTY = 'reconnectQueries'; + function getOrigin(url: string | undefined): string | null { if (!url) return null; try { @@ -46,7 +49,7 @@ function createNonArcSnapshot(pageOrigin: string | null): ArcContextSnapshot { function detectArcContextFromPage(): ArcContextDetectionResult { function sanitize(value: unknown, depth = 0): unknown { - if (depth > 6) return null; + if (depth > MAX_SANITIZATION_DEPTH) return null; if (value === null || value === undefined) return value; if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value; if (Array.isArray(value)) return value.map(item => sanitize(item, depth + 1)); @@ -113,12 +116,13 @@ function detectArcContextFromPage(): ArcContextDetectionResult { }; } - const visited = new Set(); + const visited = new Set(); const queue: unknown[] = [(root as unknown as Record)[fiberKey]]; while (queue.length > 0) { const node = queue.shift() as Record | undefined; - if (!node || visited.has(node)) continue; + if (!node) continue; + if (visited.has(node)) continue; visited.add(node); const memoizedProps = node.memoizedProps as Record | undefined; @@ -127,7 +131,7 @@ function detectArcContextFromPage(): ArcContextDetectionResult { if ( contextValue && typeof contextValue === 'object' && - 'reconnectQueries' in (contextValue as Record) + ARC_CONTEXT_MARKER_PROPERTY in (contextValue as Record) ) { const context = contextValue as Record; const configuration = sanitize(context.configuration) as Record | null; From dd19c0487904be5d80b94d301e9b5d2a398c6678 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 06:40:27 +0000 Subject: [PATCH 4/6] Address review feedback for Arc context changes Agent-Logs-Url: https://github.com/Cratis/Lens/sessions/04c942fe-6efe-4d76-9b16-f847802ce2c1 Co-authored-by: einari <134365+einari@users.noreply.github.com> --- Source/src/options/Options.tsx | 10 ++++------ Source/src/options/components/ArcSettings.tsx | 2 +- Source/src/shared/arc-context.ts | 1 + 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Source/src/options/Options.tsx b/Source/src/options/Options.tsx index 71f5243..d6ca92f 100644 --- a/Source/src/options/Options.tsx +++ b/Source/src/options/Options.tsx @@ -17,6 +17,7 @@ export function Options() { const [arcContextLoading, setArcContextLoading] = useState(true); const [activeTab, setActiveTab] = useState('users'); const [saved, setSaved] = useState(false); + const hasArcContext = arcContext?.isArcApplication === true; useEffect(() => { getSettings().then(setSettings); @@ -26,11 +27,9 @@ export function Options() { }, []); useEffect(() => { - const arcAvailable = arcContext?.isArcApplication === true; - if (!arcAvailable && (activeTab === 'commands' || activeTab === 'queries')) { - setActiveTab('arc'); - } - }, [arcContext, activeTab]); + if (hasArcContext) return; + setActiveTab(current => (current === 'commands' || current === 'queries' ? 'arc' : current)); + }, [hasArcContext]); const handleChange = async (updated: ExtensionSettings) => { setSettings(updated); @@ -43,7 +42,6 @@ export function Options() { return
Loading settings…
; } - const hasArcContext = arcContext?.isArcApplication === true; const arcBaseUrl = arcContext?.baseUrl ?? ''; const tabs: { id: Tab; label: string }[] = [ diff --git a/Source/src/options/components/ArcSettings.tsx b/Source/src/options/components/ArcSettings.tsx index fd3c5a8..c1c95c6 100644 --- a/Source/src/options/components/ArcSettings.tsx +++ b/Source/src/options/components/ArcSettings.tsx @@ -27,7 +27,7 @@ export function ArcSettings({ settings, onChange, arcContext }: Props) { {arcContext?.isArcApplication && arcContext.baseUrl ? ( <>

- Commands and queries use Arc context detected from the current page. + Commands and Queries use Arc context detected from the current page.

{arcContext.baseUrl} diff --git a/Source/src/shared/arc-context.ts b/Source/src/shared/arc-context.ts index 3c6bd56..a991358 100644 --- a/Source/src/shared/arc-context.ts +++ b/Source/src/shared/arc-context.ts @@ -175,6 +175,7 @@ export async function captureArcContextForActiveTab(): Promise Date: Mon, 25 May 2026 06:41:29 +0000 Subject: [PATCH 5/6] Harden Arc fiber traversal object handling Agent-Logs-Url: https://github.com/Cratis/Lens/sessions/04c942fe-6efe-4d76-9b16-f847802ce2c1 Co-authored-by: einari <134365+einari@users.noreply.github.com> --- Source/src/shared/arc-context.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Source/src/shared/arc-context.ts b/Source/src/shared/arc-context.ts index a991358..a58c1d3 100644 --- a/Source/src/shared/arc-context.ts +++ b/Source/src/shared/arc-context.ts @@ -18,6 +18,10 @@ interface ArcContextDetectionResult { const MAX_SANITIZATION_DEPTH = 6; const ARC_CONTEXT_MARKER_PROPERTY = 'reconnectQueries'; +function isObject(value: unknown): value is object { + return typeof value === 'object' && value !== null; +} + function getOrigin(url: string | undefined): string | null { if (!url) return null; try { @@ -116,8 +120,9 @@ function detectArcContextFromPage(): ArcContextDetectionResult { }; } - const visited = new Set(); - const queue: unknown[] = [(root as unknown as Record)[fiberKey]]; + const visited = new WeakSet(); + const fiberNode = (root as unknown as Record)[fiberKey]; + const queue: object[] = isObject(fiberNode) ? [fiberNode] : []; while (queue.length > 0) { const node = queue.shift() as Record | undefined; @@ -148,9 +153,9 @@ function detectArcContextFromPage(): ArcContextDetectionResult { const child = node.child; const sibling = node.sibling; const parent = node.return; - if (child && !visited.has(child)) queue.push(child); - if (sibling && !visited.has(sibling)) queue.push(sibling); - if (parent && !visited.has(parent)) queue.push(parent); + if (isObject(child) && !visited.has(child)) queue.push(child); + if (isObject(sibling) && !visited.has(sibling)) queue.push(sibling); + if (isObject(parent) && !visited.has(parent)) queue.push(parent); } return { From b5fc482ea980c0b7d195330b1992cc96f584922b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 06:42:51 +0000 Subject: [PATCH 6/6] Polish Arc context UI flow and shared constants Agent-Logs-Url: https://github.com/Cratis/Lens/sessions/04c942fe-6efe-4d76-9b16-f847802ce2c1 Co-authored-by: einari <134365+einari@users.noreply.github.com> --- Source/src/options/Options.tsx | 18 +++++++----------- .../src/options/components/CommandsPanel.tsx | 3 ++- Source/src/options/components/QueriesPanel.tsx | 3 ++- .../options/components/arc-panel-constants.ts | 1 + Source/src/shared/arc-context.ts | 4 +++- 5 files changed, 15 insertions(+), 14 deletions(-) create mode 100644 Source/src/options/components/arc-panel-constants.ts diff --git a/Source/src/options/Options.tsx b/Source/src/options/Options.tsx index d6ca92f..76b8bf7 100644 --- a/Source/src/options/Options.tsx +++ b/Source/src/options/Options.tsx @@ -26,11 +26,6 @@ export function Options() { .finally(() => setArcContextLoading(false)); }, []); - useEffect(() => { - if (hasArcContext) return; - setActiveTab(current => (current === 'commands' || current === 'queries' ? 'arc' : current)); - }, [hasArcContext]); - const handleChange = async (updated: ExtensionSettings) => { setSettings(updated); await saveSettings(updated); @@ -53,6 +48,7 @@ export function Options() { tabs.push({ id: 'commands', label: 'Commands' }); tabs.push({ id: 'queries', label: 'Queries' }); } + const resolvedActiveTab = tabs.some(tab => tab.id === activeTab) ? activeTab : 'users'; return (
@@ -65,7 +61,7 @@ export function Options() { {tabs.map(t => (
)} - {activeTab === 'users' && ( + {resolvedActiveTab === 'users' && ( )} - {activeTab === 'tenants' && ( + {resolvedActiveTab === 'tenants' && ( )} - {activeTab === 'arc' && ( + {resolvedActiveTab === 'arc' && ( )} - {activeTab === 'commands' && ( + {resolvedActiveTab === 'commands' && hasArcContext && ( )} - {activeTab === 'queries' && ( + {resolvedActiveTab === 'queries' && hasArcContext && ( )} diff --git a/Source/src/options/components/CommandsPanel.tsx b/Source/src/options/components/CommandsPanel.tsx index 4f9144f..da4b8a6 100644 --- a/Source/src/options/components/CommandsPanel.tsx +++ b/Source/src/options/components/CommandsPanel.tsx @@ -1,5 +1,6 @@ import { useState, useCallback, useEffect } from 'react'; import { CommandIntrospectionMetadata } from '../../shared/types'; +import { ARC_CONTEXT_UNAVAILABLE_MESSAGE } from './arc-panel-constants'; interface Props { arcBaseUrl: string; @@ -27,7 +28,7 @@ export function CommandsPanel({ arcBaseUrl }: Props) { const fetchCommands = useCallback(async () => { if (!arcBaseUrl) { setCommands(null); - setFetchError('Arc base URL unavailable. Open Lens on an Arc application page.'); + setFetchError(ARC_CONTEXT_UNAVAILABLE_MESSAGE); return; } diff --git a/Source/src/options/components/QueriesPanel.tsx b/Source/src/options/components/QueriesPanel.tsx index 0c695f1..760e5f3 100644 --- a/Source/src/options/components/QueriesPanel.tsx +++ b/Source/src/options/components/QueriesPanel.tsx @@ -1,5 +1,6 @@ import { useState, useCallback, useEffect } from 'react'; import { QueryIntrospectionMetadata } from '../../shared/types'; +import { ARC_CONTEXT_UNAVAILABLE_MESSAGE } from './arc-panel-constants'; interface Props { arcBaseUrl: string; @@ -40,7 +41,7 @@ export function QueriesPanel({ arcBaseUrl }: Props) { const fetchQueries = useCallback(async () => { if (!arcBaseUrl) { setQueries(null); - setFetchError('Arc base URL unavailable. Open Lens on an Arc application page.'); + setFetchError(ARC_CONTEXT_UNAVAILABLE_MESSAGE); return; } diff --git a/Source/src/options/components/arc-panel-constants.ts b/Source/src/options/components/arc-panel-constants.ts new file mode 100644 index 0000000..5e94583 --- /dev/null +++ b/Source/src/options/components/arc-panel-constants.ts @@ -0,0 +1 @@ +export const ARC_CONTEXT_UNAVAILABLE_MESSAGE = 'Arc base URL unavailable. Open Lens on an Arc application page.'; diff --git a/Source/src/shared/arc-context.ts b/Source/src/shared/arc-context.ts index a58c1d3..f28e70b 100644 --- a/Source/src/shared/arc-context.ts +++ b/Source/src/shared/arc-context.ts @@ -80,6 +80,8 @@ function detectArcContextFromPage(): ArcContextDetectionResult { function getConfiguredBaseUrl(configuration: Record | null, pageOrigin: string): string | null { if (!configuration) return null; + // Arc apps may expose base URL under different configuration names. + // We check the most explicit/common names first. const candidates = [ configuration.baseUrl, configuration.apiBaseUrl, @@ -125,7 +127,7 @@ function detectArcContextFromPage(): ArcContextDetectionResult { const queue: object[] = isObject(fiberNode) ? [fiberNode] : []; while (queue.length > 0) { - const node = queue.shift() as Record | undefined; + const node = queue.pop() as Record | undefined; if (!node) continue; if (visited.has(node)) continue; visited.add(node);