From 2904cf849d65e4d4f82934d4f714cc3314ed8a5d Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Sun, 23 Aug 2026 19:06:53 -0300 Subject: [PATCH 1/4] fix(security): clear new CodeQL code-scanning alerts (round 4) (#11293) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - open-sse/executors/github.ts: replace the Math.random() fallback in the Copilot correlation-id generators (x-request-id, x-interaction-id, x-client-session-id, x-agent-task-id) with a CSPRNG-backed randomIdFallback() (node:crypto randomBytes) — closes js/insecure-randomness with no behavior change (crypto.randomUUID stays the primary path). - tests/unit/cli/_helpers/shellArgs.mjs: collapse the two sequential global .replace() unescape passes into a single left-to-right regex replace with alternation — closes js/double-escaping. The prior two-pass form let the first pass's output feed the second, which is exactly the double-(un)escaping bug pattern the query flags (e.g. an escaped-backslash-then-quote sequence could be misread depending on pass order). Co-authored-by: Markus Hartung --- open-sse/executors/github.ts | 11 +++++++++-- tests/unit/cli/_helpers/shellArgs.mjs | 7 +++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/open-sse/executors/github.ts b/open-sse/executors/github.ts index f3ce1196bf..6211b509a0 100644 --- a/open-sse/executors/github.ts +++ b/open-sse/executors/github.ts @@ -1,3 +1,5 @@ +import { randomBytes } from "node:crypto"; + import { BaseExecutor, ExecuteInput, @@ -13,6 +15,11 @@ import { import { sanitizeResponsesInputItems } from "../services/responsesInputSanitizer.ts"; import { stripUnsupportedParams } from "../translator/paramSupport.ts"; +/** Correlation-id fallback for runtimes without crypto.randomUUID — still CSPRNG-backed. */ +function randomIdFallback(): string { + return `${Date.now()}-${randomBytes(9).toString("hex")}`; +} + /** * What a Copilot credential refresh resolves to. * @@ -329,7 +336,7 @@ export class GithubExecutor extends BaseExecutor { ...getGitHubCopilotChatHeaders(stream ? "text/event-stream" : "application/json", initiator), Authorization: `Bearer ${token}`, "x-request-id": - crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`, + crypto.randomUUID?.() || randomIdFallback(), }; // Per-call / per-conversation / per-turn correlation ids the @github/copilot @@ -338,7 +345,7 @@ export class GithubExecutor extends BaseExecutor { // fresh uuids. A Copilot-aware client may pin the session/task ids across a // conversation via its own headers — honor those when present, else mint. const genId = () => - crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`; + crypto.randomUUID?.() || randomIdFallback(); headers["x-interaction-id"] = this.readClientHeader(clientHeaders, "x-interaction-id") || genId(); headers["x-client-session-id"] = this.readClientHeader(clientHeaders, "x-client-session-id") || genId(); diff --git a/tests/unit/cli/_helpers/shellArgs.mjs b/tests/unit/cli/_helpers/shellArgs.mjs index 148fb94174..d5db0416e7 100644 --- a/tests/unit/cli/_helpers/shellArgs.mjs +++ b/tests/unit/cli/_helpers/shellArgs.mjs @@ -23,8 +23,11 @@ export function unescapeWindowsShellArg(arg) { s = s.replace(/\^(.)/g, "$1").replace(/\^(.)/g, "$1"); // 2. drop the wrapping quotes added by the CRT argv layer if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) s = s.slice(1, -1); - // 3. undo the doubled backslashes and the escaped embedded quotes - s = s.replace(/\\\\/g, "\\").replace(/\\"/g, '"'); + // 3. undo the doubled backslashes and the escaped embedded quotes in a single + // left-to-right pass — two sequential global replaces would let the first + // pass's output feed the second (e.g. an escaped-backslash-then-quote + // sequence could be misread), which is exactly what js/double-escaping flags. + s = s.replace(/\\\\|\\"/g, (m) => (m === "\\\\" ? "\\" : '"')); return s; } From d137368fb57c65e538204fb78df93fbfad5f479f Mon Sep 17 00:00:00 2001 From: engenhariaandrereis01-ai Date: Sun, 23 Aug 2026 19:08:18 -0300 Subject: [PATCH 2/4] fix(oauth): stop overwriting Kiro connections that share a profile ARN (#10815) (#11287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validated on a 3-PR combined board: kiro-connection-identity 8/8 (written failing-first — 3 new cases red on the pristine release/v3.8.50 tip, green with this change), typecheck:core + dashboard-typecheck clean, all static gates within baseline. Root cause is exactly right: a CodeWhisperer profile ARN identifies the profile, not the account, and distinct Builder ID accounts via social login can share one — the ARN is now trusted only alongside a non-contradicting account-level identifier (email/clientId). Closes #10815. Thank you @engenhariaandrereis01-ai! --- .../fixes/10815-kiro-social-multi-account.md | 1 + src/lib/oauth/kiroConnectionIdentity.ts | 30 +++++++++++- tests/unit/kiro-connection-identity.test.ts | 46 +++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fixes/10815-kiro-social-multi-account.md diff --git a/changelog.d/fixes/10815-kiro-social-multi-account.md b/changelog.d/fixes/10815-kiro-social-multi-account.md new file mode 100644 index 0000000000..45b2cfed59 --- /dev/null +++ b/changelog.d/fixes/10815-kiro-social-multi-account.md @@ -0,0 +1 @@ +- fix(oauth): stop treating the Kiro profile ARN as an account identity in `findKiroConnectionByIdentity()`, so a second Google/GitHub social login creates a new connection instead of overwriting the first — distinct Builder ID accounts share the same CodeWhisperer profile ARN, and the social token is not a JWT, so no e-mail was available to disambiguate them (#10815) diff --git a/src/lib/oauth/kiroConnectionIdentity.ts b/src/lib/oauth/kiroConnectionIdentity.ts index d5ff76157f..7a2a801b30 100644 --- a/src/lib/oauth/kiroConnectionIdentity.ts +++ b/src/lib/oauth/kiroConnectionIdentity.ts @@ -30,6 +30,27 @@ function providerData(connection: KiroConnectionLike): Record { : {}; } +/** True when the identity carries something that identifies the ACCOUNT (not the profile). */ +function hasAccountIdentifier(identity: KiroConnectionIdentity): boolean { + return Boolean(folded(identity.email) || trimmed(identity.clientId)); +} + +/** True when a shared field is present on both sides and disagrees — different accounts. */ +function contradictsAccount( + connection: KiroConnectionLike, + identity: KiroConnectionIdentity +): boolean { + const email = folded(identity.email); + const existingEmail = folded(connection.email); + if (email && existingEmail && email !== existingEmail) return true; + + const clientId = trimmed(identity.clientId); + const existingClientId = trimmed(providerData(connection).clientId); + if (clientId && existingClientId && clientId !== existingClientId) return true; + + return false; +} + /** Find an existing Kiro account without comparing OAuth tokens or API keys. */ export function findKiroConnectionByIdentity( connections: KiroConnectionLike[], @@ -45,7 +66,14 @@ export function findKiroConnectionByIdentity( const match = candidates.find( (connection) => trimmed(providerData(connection).profileArn) === profileArn ); - if (match) return match; + // A profile ARN identifies the CodeWhisperer PROFILE, not the account: distinct + // Builder ID accounts (Google/GitHub social login) share the same ARN. Accepting it + // as identity made a second social login overwrite the first connection (#10815). + // Only trust the ARN when the incoming identity carries an account-level identifier + // that does not contradict the stored one. + if (match && hasAccountIdentifier(identity) && !contradictsAccount(match, identity)) { + return match; + } } const clientId = trimmed(identity.clientId); diff --git a/tests/unit/kiro-connection-identity.test.ts b/tests/unit/kiro-connection-identity.test.ts index c5ac2b134f..bcda14c1db 100644 --- a/tests/unit/kiro-connection-identity.test.ts +++ b/tests/unit/kiro-connection-identity.test.ts @@ -79,3 +79,49 @@ test("findKiroConnectionByIdentity never overwrites a different authentication t null ); }); + +// #10815 — a profile ARN identifies the CodeWhisperer profile, not the account: two +// distinct social (Google/GitHub) Builder ID accounts share the same ARN, so matching +// on it alone made the second login overwrite the first connection. +const SHARED_PROFILE_ARN = "arn:aws:codewhisperer:us-east-1:1:profile/SHARED"; + +const firstSocialAccount = { + id: "social-account-1", + authType: "oauth", + name: null, + email: null, + providerSpecificData: { + profileArn: SHARED_PROFILE_ARN, + authMethod: "imported", + provider: "Github", + }, +}; + +test("findKiroConnectionByIdentity does not match a shared profile ARN without an account identifier", () => { + const match = findKiroConnectionByIdentity([firstSocialAccount], { + authType: "oauth", + profileArn: SHARED_PROFILE_ARN, + email: null, + }); + assert.equal(match, null); +}); + +test("findKiroConnectionByIdentity treats diverging emails on a shared profile ARN as distinct accounts", () => { + const stored = { ...firstSocialAccount, id: "social-a", email: "a@example.com" }; + const match = findKiroConnectionByIdentity([stored], { + authType: "oauth", + profileArn: SHARED_PROFILE_ARN, + email: "b@example.com", + }); + assert.equal(match, null); +}); + +test("findKiroConnectionByIdentity still matches the same account on a shared profile ARN", () => { + const stored = { ...firstSocialAccount, id: "social-a", email: "a@example.com" }; + const match = findKiroConnectionByIdentity([stored], { + authType: "oauth", + profileArn: SHARED_PROFILE_ARN, + email: "a@example.com", + }); + assert.equal(match?.id, "social-a"); +}); From 7cec8e32fde1e7bd0ab858aee62a1d3eb9c54e3c Mon Sep 17 00:00:00 2001 From: ignamiranda <34501347+ignamiranda@users.noreply.github.com> Date: Sun, 23 Aug 2026 18:08:23 -0400 Subject: [PATCH 3/4] Beginner UX: Essentials sidebar preset (#11286) Validated on a 3-PR combined board: sidebar-customization + sidebar-essentials-static 30/30 combined (across the board's 4 focused files), typecheck:core + dashboard-typecheck clean, gates within baseline. New Essentials sidebar preset gives first-time users a short beginner path while advanced tools stay reachable via Command Palette search. Thank you @ignamiranda! --- .../11286-essentials-sidebar-preset.md | 1 + .../settings/components/SidebarTab.tsx | 5 +++ src/i18n/messages/en.json | 12 +++++ src/shared/components/CommandPalette.tsx | 22 ++++++++- src/shared/constants/sidebarVisibility.ts | 31 +++++++++++++ .../constants/sidebarVisibility/types.ts | 2 +- src/shared/validation/settingsSchemas.ts | 5 ++- tests/unit/sidebar-customization.test.ts | 27 ++++++++++- tests/unit/sidebar-essentials-static.test.ts | 45 +++++++++++++++++++ 9 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 changelog.d/features/11286-essentials-sidebar-preset.md create mode 100644 tests/unit/sidebar-essentials-static.test.ts diff --git a/changelog.d/features/11286-essentials-sidebar-preset.md b/changelog.d/features/11286-essentials-sidebar-preset.md new file mode 100644 index 0000000000..f05152001a --- /dev/null +++ b/changelog.d/features/11286-essentials-sidebar-preset.md @@ -0,0 +1 @@ +- **feat(dashboard):** add an Essentials sidebar preset that shows only the beginner core path (Home → Endpoints → API Keys → Providers → Health → Settings) while keeping Advanced tools reachable via Command Palette search ([#11286](https://github.com/diegosouzapw/OmniRoute/pull/11286)) diff --git a/src/app/(dashboard)/dashboard/settings/components/SidebarTab.tsx b/src/app/(dashboard)/dashboard/settings/components/SidebarTab.tsx index e18611ff2c..af8583b1c1 100644 --- a/src/app/(dashboard)/dashboard/settings/components/SidebarTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/SidebarTab.tsx @@ -521,6 +521,7 @@ export default function SidebarTab() { const presetLabels: Record = { all: getSettingsLabel("presetAll", "All"), + essentials: getSettingsLabel("presetEssentials", "Essentials"), minimal: getSettingsLabel("presetMinimal", "Minimal"), developer: getSettingsLabel("presetDeveloper", "Developer"), admin: getSettingsLabel("presetAdmin", "Admin"), @@ -528,6 +529,10 @@ export default function SidebarTab() { const presetDescriptions: Record = { all: getSettingsLabel("presetAllDesc", "Show everything"), + essentials: getSettingsLabel( + "presetEssentialsDesc", + "Beginner path — Advanced tools stay searchable" + ), minimal: getSettingsLabel("presetMinimalDesc", "Core pages only"), developer: getSettingsLabel("presetDeveloperDesc", "Dev & proxy tools"), admin: getSettingsLabel("presetAdminDesc", "Monitoring & audit"), diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 4aaaf250cc..6a6ec59863 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -6707,6 +6707,18 @@ "sidebarVisibility": "Hide sidebar items", "sidebarVisibilityDesc": "Hide any sidebar navigation entry to reduce visual clutter without disabling any features", "sidebarVisibilityHint": "Any sidebar section is hidden automatically when all of its entries are hidden", + "presetAll": "All", + "presetAllDesc": "Show everything", + "presetEssentials": "Essentials", + "presetEssentialsDesc": "Beginner path - Advanced tools stay searchable", + "presetMinimal": "Minimal", + "presetMinimalDesc": "Core pages only", + "presetDeveloper": "Developer", + "presetDeveloperDesc": "Dev & proxy tools", + "presetAdmin": "Admin", + "presetAdminDesc": "Monitoring & audit", + "settingsSidebarTitle": "Sidebar Customization", + "settingsSidebarDesc": "Choose which sidebar items to show. Essentials keeps Advanced tools searchable.", "hideHealthLogs": "Hide Health Check Logs", "hideHealthLogsDesc": "When ON, suppress [HealthCheck] messages in server console", "themeAccent": "Theme color", diff --git a/src/shared/components/CommandPalette.tsx b/src/shared/components/CommandPalette.tsx index 0f4868b25c..096b57b287 100644 --- a/src/shared/components/CommandPalette.tsx +++ b/src/shared/components/CommandPalette.tsx @@ -6,8 +6,11 @@ import { useTranslations } from "next-intl"; import { SIDEBAR_SECTIONS, HIDDEN_SIDEBAR_ITEMS_SETTING_KEY, + SIDEBAR_PRESET_KEY, + ESSENTIALS_ADVANCED_TOOL_IDS, normalizeHiddenSidebarItems, resolveRuntimeSidebarSections, + type HideableSidebarItemId, type SidebarItemDefinition, type SidebarSectionChild, } from "@/shared/constants/sidebarVisibility"; @@ -61,6 +64,7 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) { const [query, setQuery] = useState(""); const [selectedIndex, setSelectedIndex] = useState(0); const [hiddenItems, setHiddenItems] = useState>(new Set()); + const [activePreset, setActivePreset] = useState(null); const [radarAdminUrl, setRadarAdminUrl] = useState(null); useEffect(() => { @@ -71,6 +75,9 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) { setHiddenItems( new Set(normalizeHiddenSidebarItems(data?.[HIDDEN_SIDEBAR_ITEMS_SETTING_KEY])) ); + setActivePreset( + typeof data?.[SIDEBAR_PRESET_KEY] === "string" ? data[SIDEBAR_PRESET_KEY] : null + ); setRadarAdminUrl(data?.radarAdminUrl ?? null); }) .catch(() => { @@ -104,7 +111,13 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) { if (isSidebarGroup(child)) { const subgroupLabel = safeTranslate(child.titleKey, child.titleFallback); return child.items - .filter((item) => !hiddenItems.has(item.id)) + .filter((item) => { + if (!hiddenItems.has(item.id)) return true; + return ( + activePreset === "essentials" && + ESSENTIALS_ADVANCED_TOOL_IDS.has(item.id as HideableSidebarItemId) + ); + }) .map((item) => ({ id: item.id, href: item.href, @@ -121,7 +134,12 @@ function CommandPaletteDialog({ onClose }: { onClose: () => void }) { })); } const item = child as SidebarItemDefinition; - if (hiddenItems.has(item.id)) return []; + if (hiddenItems.has(item.id)) { + const keepForEssentials = + activePreset === "essentials" && + ESSENTIALS_ADVANCED_TOOL_IDS.has(item.id as HideableSidebarItemId); + if (!keepForEssentials) return []; + } return [ { id: item.id, diff --git a/src/shared/constants/sidebarVisibility.ts b/src/shared/constants/sidebarVisibility.ts index 81e256038a..270715ef42 100644 --- a/src/shared/constants/sidebarVisibility.ts +++ b/src/shared/constants/sidebarVisibility.ts @@ -202,6 +202,36 @@ export const SIDEBAR_ITEM_ORDER_KEY = "sidebarItemOrder"; export const SIDEBAR_PRESET_KEY = "sidebarActivePreset"; export const SIDEBAR_SETTINGS_UPDATED_EVENT = "omniroute:settings-updated"; +/** Beginner Essentials: core path only. Advanced tools stay reachable via search. */ +const ESSENTIALS_SHOWN: ReadonlySet = new Set([ + "home", + "endpoints", + "api-manager", + "providers", + "health", + "settings-general", + "settings-sidebar", +]); + +/** Hidden in Essentials sidebar but kept searchable in Command Palette. */ +export const ESSENTIALS_ADVANCED_TOOL_IDS: ReadonlySet = new Set([ + "playground", + "logs", + "batch", + "translator", + "combos", + "quota", + "analytics", + "costs", + "cache", + "runtime", + "resilience-connections", + "mcp", + "a2a", + "memory", + "skills", +]); + const MINIMAL_SHOWN: ReadonlySet = new Set([ "home", "endpoints", @@ -297,6 +327,7 @@ function buildHiddenList(shown: ReadonlySet): HideableSid export const SIDEBAR_PRESETS: readonly SidebarPresetDefinition[] = [ { id: "all", icon: "select_all", hiddenItems: [] }, + { id: "essentials", icon: "star", hiddenItems: buildHiddenList(ESSENTIALS_SHOWN) }, { id: "minimal", icon: "minimize", hiddenItems: buildHiddenList(MINIMAL_SHOWN) }, { id: "developer", icon: "code", hiddenItems: buildHiddenList(DEVELOPER_SHOWN) }, { id: "admin", icon: "admin_panel_settings", hiddenItems: buildHiddenList(ADMIN_SHOWN) }, diff --git a/src/shared/constants/sidebarVisibility/types.ts b/src/shared/constants/sidebarVisibility/types.ts index 3bb6330ed0..159e6d0349 100644 --- a/src/shared/constants/sidebarVisibility/types.ts +++ b/src/shared/constants/sidebarVisibility/types.ts @@ -174,7 +174,7 @@ export interface SidebarSectionDefinition { defaultPinned?: boolean; } -export type SidebarPresetId = "all" | "minimal" | "developer" | "admin"; +export type SidebarPresetId = "all" | "essentials" | "minimal" | "developer" | "admin"; export interface SidebarPresetDefinition { id: SidebarPresetId; diff --git a/src/shared/validation/settingsSchemas.ts b/src/shared/validation/settingsSchemas.ts index beefb5fc8f..def9327741 100644 --- a/src/shared/validation/settingsSchemas.ts +++ b/src/shared/validation/settingsSchemas.ts @@ -199,7 +199,10 @@ export const updateSettingsSchema = z.object({ .array(z.enum(SIDEBAR_SECTIONS.map((s) => s.id) as [string, ...string[]])) .optional(), sidebarItemOrder: z.record(z.string(), z.array(z.string().max(100))).optional(), - sidebarActivePreset: z.enum(["all", "minimal", "developer", "admin"]).nullable().optional(), + sidebarActivePreset: z + .enum(["all", "essentials", "minimal", "developer", "admin"]) + .nullable() + .optional(), comboConfigMode: z.enum(COMBO_CONFIG_MODES).optional(), codexServiceTier: z .object({ diff --git a/tests/unit/sidebar-customization.test.ts b/tests/unit/sidebar-customization.test.ts index 405265d71b..8c612bcd68 100644 --- a/tests/unit/sidebar-customization.test.ts +++ b/tests/unit/sidebar-customization.test.ts @@ -89,9 +89,10 @@ test("applyItemOrder ignores unknown IDs in order list", () => { // ─── SIDEBAR_PRESETS ────────────────────────────────────────────────────────── -test("SIDEBAR_PRESETS contains all four preset IDs", () => { +test("SIDEBAR_PRESETS contains all five preset IDs", () => { const ids = SIDEBAR_PRESETS.map((p) => p.id); assert.ok(ids.includes("all"), "expected 'all' preset"); + assert.ok(ids.includes("essentials"), "expected 'essentials' preset"); assert.ok(ids.includes("minimal"), "expected 'minimal' preset"); assert.ok(ids.includes("developer"), "expected 'developer' preset"); assert.ok(ids.includes("admin"), "expected 'admin' preset"); @@ -112,6 +113,30 @@ test("SIDEBAR_PRESETS 'all' preset has no hidden items", () => { assert.deepEqual(allPreset.hiddenItems, []); }); +test("SIDEBAR_PRESETS includes essentials as the beginner path", () => { + assert.equal(SIDEBAR_PRESETS.length, 5); + assert.deepEqual( + SIDEBAR_PRESETS.map((p) => p.id), + ["all", "essentials", "minimal", "developer", "admin"] + ); + const essentials = SIDEBAR_PRESETS.find((p) => p.id === "essentials"); + assert.ok(essentials, "expected 'essentials' preset to exist"); + const hidden = new Set(essentials.hiddenItems); + for (const id of [ + "home", + "endpoints", + "api-manager", + "providers", + "health", + "settings-general", + "settings-sidebar", + ]) { + assert.equal(hidden.has(id as never), false, `${id} should stay visible in essentials`); + } + assert.equal(hidden.has("playground"), true); + assert.equal(hidden.has("logs"), true); +}); + test("SIDEBAR_PRESETS non-all presets have at least one hidden item", () => { for (const preset of SIDEBAR_PRESETS.filter((p) => p.id !== "all")) { assert.ok(preset.hiddenItems.length > 0, `Preset '${preset.id}' should hide at least one item`); diff --git a/tests/unit/sidebar-essentials-static.test.ts b/tests/unit/sidebar-essentials-static.test.ts new file mode 100644 index 0000000000..f7da731f27 --- /dev/null +++ b/tests/unit/sidebar-essentials-static.test.ts @@ -0,0 +1,45 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); + +test("essentials preset is registered in sidebar visibility types and presets", () => { + const types = fs.readFileSync( + path.join(repoRoot, "src/shared/constants/sidebarVisibility/types.ts"), + "utf8" + ); + const visibility = fs.readFileSync( + path.join(repoRoot, "src/shared/constants/sidebarVisibility.ts"), + "utf8" + ); + const schema = fs.readFileSync( + path.join(repoRoot, "src/shared/validation/settingsSchemas.ts"), + "utf8" + ); + + assert.match(types, /"essentials"/); + assert.match(visibility, /id:\s*"essentials"/); + assert.match(visibility, /ESSENTIALS_ADVANCED_TOOL_IDS/); + assert.match(schema, /"essentials"/); +}); + +test("command palette keeps essentials advanced tools searchable", () => { + const source = fs.readFileSync( + path.join(repoRoot, "src/shared/components/CommandPalette.tsx"), + "utf8" + ); + assert.match(source, /ESSENTIALS_ADVANCED_TOOL_IDS/); + assert.match(source, /activePreset === "essentials"/); +}); + +test("essentials i18n keys exist in en.json", () => { + const en = JSON.parse( + fs.readFileSync(path.join(repoRoot, "src/i18n/messages/en.json"), "utf8") + ) as { settings: Record }; + assert.equal(en.settings.presetEssentials, "Essentials"); + assert.match(en.settings.presetEssentialsDesc, /Beginner path/i); + assert.match(en.settings.presetEssentialsDesc, /searchable/i); +}); From 92f58603f9860c2031f4527b464bd753c6d4918f Mon Sep 17 00:00:00 2001 From: ignamiranda <34501347+ignamiranda@users.noreply.github.com> Date: Sun, 23 Aug 2026 18:08:27 -0400 Subject: [PATCH 4/4] Beginner UX: purpose-first Traffic Inspector header (#11283) Validated on a 3-PR combined board: traffic-inspector-beginner-header suite green within the board's 30/30, typecheck:core + dashboard-typecheck clean, gates within baseline. Purpose-first orientation header for Traffic Inspector, existing inspection UI untouched. Thank you @ignamiranda! --- .../11283-traffic-inspector-purpose-header.md | 1 + .../TrafficInspectorPageClient.tsx | 22 +++++++++- .../tools/traffic-inspector/page.tsx | 5 ++- src/i18n/messages/en.json | 3 +- .../traffic-inspector-beginner-header.test.ts | 44 +++++++++++++++++++ 5 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 changelog.d/features/11283-traffic-inspector-purpose-header.md create mode 100644 tests/unit/traffic-inspector-beginner-header.test.ts diff --git a/changelog.d/features/11283-traffic-inspector-purpose-header.md b/changelog.d/features/11283-traffic-inspector-purpose-header.md new file mode 100644 index 0000000000..4faaf5bc57 --- /dev/null +++ b/changelog.d/features/11283-traffic-inspector-purpose-header.md @@ -0,0 +1 @@ +- **feat(dashboard):** lead Traffic Inspector with a purpose-first header that separates "what happened" from "how it happened", so beginners can read request outcomes without drowning in protocol detail ([#11283](https://github.com/diegosouzapw/OmniRoute/pull/11283)) diff --git a/src/app/(dashboard)/dashboard/tools/traffic-inspector/TrafficInspectorPageClient.tsx b/src/app/(dashboard)/dashboard/tools/traffic-inspector/TrafficInspectorPageClient.tsx index a1537f236d..cd3a33372a 100644 --- a/src/app/(dashboard)/dashboard/tools/traffic-inspector/TrafficInspectorPageClient.tsx +++ b/src/app/(dashboard)/dashboard/tools/traffic-inspector/TrafficInspectorPageClient.tsx @@ -15,7 +15,15 @@ import { HistoricSessionBanner } from "./components/session/HistoricSessionBanne const BUFFER_MAX = 1000; -export function TrafficInspectorPageClient() { +export function TrafficInspectorPageClient({ + title, + subtitle, + purpose, +}: { + title?: string; + subtitle?: string; + purpose?: string; +} = {}) { const [containerHeight, setContainerHeight] = useState(600); const listContainerRef = useRef(null); const [selectedRequest, setSelectedRequest] = useState(null); @@ -91,6 +99,18 @@ export function TrafficInspectorPageClient() { return (
+ {title && ( +
+

{title}

+ {subtitle && ( +

{subtitle}

+ )} + {purpose && ( +

{purpose}

+ )} +
+ )} + {/* Capture modes toolbar */}
diff --git a/src/app/(dashboard)/dashboard/tools/traffic-inspector/page.tsx b/src/app/(dashboard)/dashboard/tools/traffic-inspector/page.tsx index fb3f9ddc3d..ba2f691a2f 100644 --- a/src/app/(dashboard)/dashboard/tools/traffic-inspector/page.tsx +++ b/src/app/(dashboard)/dashboard/tools/traffic-inspector/page.tsx @@ -9,6 +9,7 @@ export async function generateMetadata() { }; } -export default function TrafficInspectorPage() { - return ; +export default async function TrafficInspectorPage() { + const t = await getTranslations("sidebar"); + return ; } diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 6a6ec59863..e45ebbd04f 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -1272,7 +1272,8 @@ "agentBridge": "Agent Bridge", "agentBridgeSubtitle": "Intercept IDE agent traffic", "trafficInspector": "Traffic Inspector", - "trafficInspectorSubtitle": "Monitor LLM calls + debug any HTTPS traffic", + "trafficInspectorSubtitle": "Inspect request and response traffic from your apps", + "trafficInspectorPurpose": "See exactly what your application sends to and receives from AI providers. Works with any OpenAI-compatible client.", "cliCode": "CLI Code", "cliCodeSubtitle": "Code tools pointing to OmniRoute", "cliAgents": "CLI Agents", diff --git a/tests/unit/traffic-inspector-beginner-header.test.ts b/tests/unit/traffic-inspector-beginner-header.test.ts new file mode 100644 index 0000000000..693597fbfc --- /dev/null +++ b/tests/unit/traffic-inspector-beginner-header.test.ts @@ -0,0 +1,44 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const pagePath = path.join( + repoRoot, + "src/app/(dashboard)/dashboard/tools/traffic-inspector/page.tsx" +); +const clientPath = path.join( + repoRoot, + "src/app/(dashboard)/dashboard/tools/traffic-inspector/TrafficInspectorPageClient.tsx" +); +const enPath = path.join(repoRoot, "src/i18n/messages/en.json"); + +test("Traffic Inspector page passes translated title, subtitle, and purpose", () => { + const pageSource = fs.readFileSync(pagePath, "utf8"); + assert.match(pageSource, /title=\{t\("trafficInspector"\)\}/); + assert.match(pageSource, /subtitle=\{t\("trafficInspectorSubtitle"\)\}/); + assert.match(pageSource, /purpose=\{t\("trafficInspectorPurpose"\)\}/); +}); + +test("Traffic Inspector client renders purpose-first header when props are provided", () => { + const clientSource = fs.readFileSync(clientPath, "utf8"); + assert.match(clientSource, /title\s*&&/); + assert.match(clientSource, /subtitle\s*&&/); + assert.match(clientSource, /purpose\s*&&/); +}); + +test("Traffic Inspector beginner i18n keys exist in en.json", () => { + const en = JSON.parse(fs.readFileSync(enPath, "utf8")); + assert.equal(en.sidebar.trafficInspector, "Traffic Inspector"); + assert.equal( + en.sidebar.trafficInspectorSubtitle, + "Inspect request and response traffic from your apps" + ); + assert.equal( + typeof en.sidebar.trafficInspectorPurpose, + "string" + ); + assert.ok(en.sidebar.trafficInspectorPurpose.length > 20); +});