From 54073d3d5d347f82357253400082b15440938a4b Mon Sep 17 00:00:00 2001 From: jeeves <308196396+cb-jeeves@users.noreply.github.com> Date: Wed, 2 Sep 2026 06:04:06 +0000 Subject: [PATCH 01/11] chore: start tool-url-scheme-warning From cc78a85b78779d0c15d3fa86e1814c1f10d8ec39 Mon Sep 17 00:00:00 2001 From: jeeves <308196396+cb-jeeves@users.noreply.github.com> Date: Wed, 2 Sep 2026 08:02:08 +0000 Subject: [PATCH 02/11] feat(tools): warn on schemeless tool URL templates and report dropped tabs honestly Closes the silent "Opened 0 tabs" path from #157. A template that does not start with http:// or https:// now gets an inline warning under the URL field in ToolEditor, in the same msg/msgWarn style the Hotkeys rows use, with an "add https://" link that prefixes the scheme, trims leading whitespace and leaves the caret after the prefix. In Tray.openTabs, when the main process opened fewer tabs than were offered, the toast reads "Opened N of M tabs; only http and https links can open" instead of a plain success count. Decisions: - The scheme check lives in src/shared/tools.ts as hasWebScheme (case- insensitive, trimmed) so the editor and any future caller agree; the main-process allowlist in open-external.ts is untouched and remains the enforcement point, per the issue. - Saving is still allowed with a schemeless template; the warning informs rather than blocks, matching the Hotkeys reserved-combination pattern. - The tray message is aggregate per click (N of M across all URLs offered), since openExternalUrls only returns a count. Files: src/shared/tools.ts, ToolEditor.tsx, Tray.tsx, plus tests for each. --- .../settings/tools/ToolEditor.test.tsx | 37 +++++++++++++++++++ .../components/settings/tools/ToolEditor.tsx | 24 +++++++++++- .../src/components/tray/Tray.test.tsx | 21 +++++++++++ src/renderer/src/components/tray/Tray.tsx | 13 ++++++- src/shared/tools.test.ts | 20 +++++++++- src/shared/tools.ts | 11 ++++++ 6 files changed, 122 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/components/settings/tools/ToolEditor.test.tsx b/src/renderer/src/components/settings/tools/ToolEditor.test.tsx index c4c90ad4..89d9826a 100644 --- a/src/renderer/src/components/settings/tools/ToolEditor.test.tsx +++ b/src/renderer/src/components/settings/tools/ToolEditor.test.tsx @@ -127,6 +127,43 @@ describe('ToolEditor', () => { expect(screen.getByTestId('readiness')).toHaveTextContent('no tokens'); expect(screen.getByTestId('tool-preview-caption')).toHaveTextContent('Would open 1 tab'); }); + + it('warns beneath the URL field when the template has no http or https scheme and prefixes https:// on click', async () => { + await renderTools( + + ); + const warning = screen.getByTestId('tool-url-scheme'); + expect(warning).toHaveTextContent('only http and https links can open'); + expect(screen.getByTestId('tool-preview-caption')).toHaveTextContent('Would open 1 tab'); + fireEvent.click(screen.getByTestId('tool-url-scheme-fix')); + expect(screen.getByTestId('tool-url')).toHaveValue('https://example.com/{email}'); + expect(screen.queryByTestId('tool-url-scheme')).not.toBeInTheDocument(); + fireEvent.change(screen.getByTestId('tool-url'), { target: { value: 'ftp://x/{email}' } }); + expect(screen.getByTestId('tool-url-scheme')).toBeInTheDocument(); + fireEvent.change(screen.getByTestId('tool-url'), { target: { value: 'HTTP://x/{email}' } }); + expect(screen.queryByTestId('tool-url-scheme')).not.toBeInTheDocument(); + fireEvent.change(screen.getByTestId('tool-url'), { target: { value: '' } }); + expect(screen.queryByTestId('tool-url-scheme')).not.toBeInTheDocument(); + }); + + it('the https:// fix strips leading whitespace and leaves the caret after the prefix', async () => { + await renderTools( + + ); + fireEvent.click(screen.getByTestId('tool-url-scheme-fix')); + const url = screen.getByTestId('tool-url') as HTMLInputElement; + expect(url).toHaveValue('https://vt.example/{ip}'); + expect(url.selectionStart).toBe('https://'.length); + expect(document.activeElement).toBe(url); + }); }); describe('TemplateEditor', () => { diff --git a/src/renderer/src/components/settings/tools/ToolEditor.tsx b/src/renderer/src/components/settings/tools/ToolEditor.tsx index 4874f398..11f4a420 100644 --- a/src/renderer/src/components/settings/tools/ToolEditor.tsx +++ b/src/renderer/src/components/settings/tools/ToolEditor.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import { useEffect, useRef, useState } from 'react'; -import { buildToolUrls } from '../../../../../shared/tools'; +import { buildToolUrls, hasWebScheme } from '../../../../../shared/tools'; import { Readiness } from './Readiness'; import { TokenPicker, insertAtCaret } from './TokenPicker'; import { TokenText } from './TokenText'; @@ -24,6 +24,7 @@ interface ToolEditorProps { } const PREVIEW_LIMIT = 4; +const WEB_PREFIX = 'https://'; /** * The tool editor (spec 14.3): name, URL, token picker, readiness line, a preview titled @@ -44,6 +45,7 @@ export function ToolEditor({ initial, onSave, onCancel }: ToolEditorProps) { const count = buildToolUrls({ url }, values).length; const resolved = resolveToolUrls(url, values); const needed = groupsNeeded({ url }); + const schemeless = url.trim().length > 0 && !hasWebScheme(url); const save = async () => { setSaving(true); @@ -79,6 +81,11 @@ export function ToolEditor({ initial, onSave, onCancel }: ToolEditorProps) { setCaret(next.caret); }; + const addScheme = () => { + setUrl(WEB_PREFIX + url.trimStart()); + setCaret(WEB_PREFIX.length); + }; + return (