From 14a6278817af4d18bf7a46f8ab27c08e9344714b Mon Sep 17 00:00:00 2001 From: Yannic Charlon <52761674+JustYannicc@users.noreply.github.com> Date: Sat, 4 Jul 2026 09:23:32 +0200 Subject: [PATCH 01/13] fix(validation): restore npm ci for esbuild platform packages --- .github/workflows/project-checks.yml | 55 ++++++++++++++++++++++++++++ package.json | 2 - 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/project-checks.yml diff --git a/.github/workflows/project-checks.yml b/.github/workflows/project-checks.yml new file mode 100644 index 00000000..41044d27 --- /dev/null +++ b/.github/workflows/project-checks.yml @@ -0,0 +1,55 @@ +name: Project Checks + +on: + pull_request: + branches: + - main + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: project-checks-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test-i18n-build: + name: Tests, i18n, and build + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + CI: true + ELECTRON_SKIP_BINARY_DOWNLOAD: '1' + SUPERCMD_SKIP_ELECTRON_TESTS: '1' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + cache-dependency-path: package-lock.json + + - name: Install dependencies without native postinstall scripts + run: npm ci --ignore-scripts + + - name: Run tests + run: npm test + + - name: Check i18n + run: npm run check:i18n + + - name: Build main process + run: npm run build:main + + - name: Build renderer + run: npm run build:renderer diff --git a/package.json b/package.json index f262a8c1..69d690bf 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,6 @@ }, "dependencies": { "@aptabase/electron": "^0.3.1", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", "@phosphor-icons/react": "^2.1.10", "@raycast/api": "^1.104.5", "electron-builder-notarize": "^1.5.2", From 6ba173bc93fa2e4aca7e45ff130dd826375a9eb3 Mon Sep 17 00:00:00 2001 From: Yannic Charlon <52761674+JustYannicc@users.noreply.github.com> Date: Sat, 4 Jul 2026 12:47:14 +0200 Subject: [PATCH 02/13] fix(validation): make macos liquid glass dependency optional --- package-lock.json | 14 ++++++++++---- package.json | 6 ++++-- scripts/ensure-macos-runtime-deps.mjs | 28 +++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 scripts/ensure-macos-runtime-deps.mjs diff --git a/package-lock.json b/package-lock.json index cbe18d8a..6d7d0d23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "supercmd", - "version": "1.0.25-PMH", + "version": "1.0.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "supercmd", - "version": "1.0.25-PMH", + "version": "1.0.26", "hasInstallScript": true, "license": "ISC", "dependencies": { @@ -14,7 +14,6 @@ "@phosphor-icons/react": "^2.1.10", "@raycast/api": "^1.104.5", "electron-builder-notarize": "^1.5.2", - "electron-liquid-glass": "^1.1.1", "electron-updater": "^6.7.3", "esbuild": "^0.19.12", "lucide-react": "^0.312.0", @@ -39,6 +38,9 @@ "typescript": "^5.3.3", "vite": "^5.0.11", "wait-on": "^7.2.0" + }, + "optionalDependencies": { + "electron-liquid-glass": "^1.1.1" } }, "node_modules/@alloc/quick-lru": { @@ -3420,6 +3422,7 @@ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", "license": "MIT", + "optional": true, "dependencies": { "file-uri-to-path": "1.0.0" } @@ -4631,6 +4634,7 @@ "resolved": "https://registry.npmjs.org/electron-liquid-glass/-/electron-liquid-glass-1.1.1.tgz", "integrity": "sha512-AfPxcu6RJYxlsW2sjgjDEON0rVOjhh5QYM6ur5hsfILQmfY5ewHCWJZJZoDsQxhjeW1DAhbRbvB79IvgW4ansA==", "license": "MIT", + "optional": true, "os": [ "darwin" ], @@ -4650,6 +4654,7 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", "license": "MIT", + "optional": true, "engines": { "node": "^18 || ^20 || >= 21" } @@ -5144,7 +5149,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/filelist": { "version": "1.0.4", diff --git a/package.json b/package.json index 69d690bf..578fd62e 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "test": "node --test 'scripts/test-*.mjs'", "build:native": "node scripts/build-native.mjs", "ensure:cross-arch-esbuild": "node scripts/ensure-cross-arch-esbuild.mjs", - "postinstall": "node scripts/ensure-cross-arch-esbuild.mjs && electron-builder install-app-deps", + "postinstall": "node scripts/ensure-macos-runtime-deps.mjs && node scripts/ensure-cross-arch-esbuild.mjs && electron-builder install-app-deps", "start": "electron .", "package": "cross-env NODE_ENV=production npm run build && electron-builder", "package:unsigned": "cross-env NODE_ENV=production CSC_IDENTITY_AUTO_DISCOVERY=false npm run build && electron-builder --mac dmg -c.mac.identity=null -c.mac.notarize=false" @@ -45,7 +45,6 @@ "@phosphor-icons/react": "^2.1.10", "@raycast/api": "^1.104.5", "electron-builder-notarize": "^1.5.2", - "electron-liquid-glass": "^1.1.1", "electron-updater": "^6.7.3", "esbuild": "^0.19.12", "lucide-react": "^0.312.0", @@ -55,6 +54,9 @@ "react-dom": "^18.2.0", "transliteration": "^2.6.1" }, + "optionalDependencies": { + "electron-liquid-glass": "^1.1.1" + }, "build": { "appId": "com.supercmd.app", "productName": "SuperCmd", diff --git a/scripts/ensure-macos-runtime-deps.mjs b/scripts/ensure-macos-runtime-deps.mjs new file mode 100644 index 00000000..10dd167d --- /dev/null +++ b/scripts/ensure-macos-runtime-deps.mjs @@ -0,0 +1,28 @@ +#!/usr/bin/env node + +import { createRequire } from 'node:module'; + +if (process.platform !== 'darwin') { + process.exit(0); +} + +const require = createRequire(import.meta.url); +const REQUIRED_PACKAGES = ['electron-liquid-glass']; + +let missing = 0; + +for (const packageName of REQUIRED_PACKAGES) { + try { + require(packageName); + } catch (error) { + missing++; + console.error(`ensure-macos-runtime-deps: failed to load ${packageName}.`); + console.error(error); + } +} + +if (missing > 0) { + process.exit(1); +} + +console.log('ensure-macos-runtime-deps: macOS runtime packages are available.'); From a3408dd3f655595f800d883c0dffb57754fa373d Mon Sep 17 00:00:00 2001 From: Yannic Charlon <52761674+JustYannicc@users.noreply.github.com> Date: Sat, 4 Jul 2026 12:45:28 +0200 Subject: [PATCH 03/13] perf(validation): speed root search perf harness --- scripts/test-root-search-perf.mjs | 375 ++++++++++++++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 scripts/test-root-search-perf.mjs diff --git a/scripts/test-root-search-perf.mjs b/scripts/test-root-search-perf.mjs new file mode 100644 index 00000000..46fd8eb2 --- /dev/null +++ b/scripts/test-root-search-perf.mjs @@ -0,0 +1,375 @@ +#!/usr/bin/env node + +import assert from 'assert/strict'; +import fs from 'fs'; +import path from 'path'; +import { createRequire } from 'module'; +import { performance } from 'perf_hooks'; +import vm from 'vm'; + +const require = createRequire(import.meta.url); +const ts = require('typescript'); + +const moduleCache = new Map(); +const ROOT = process.cwd(); + +const reactStub = { + createElement: (type, props, ...children) => ({ type, props: props || {}, children }), + Fragment: 'Fragment', +}; + +function makeComponent(name) { + return function StubComponent(props) { + return { type: name, props: props || {}, children: [] }; + }; +} + +const lucideStub = new Proxy({ __esModule: true }, { + get(target, prop) { + if (prop === '__esModule') return true; + if (prop === 'default') return target; + return makeComponent(String(prop)); + }, +}); + +function getStubbedModule(request) { + if (request === 'react') return { __esModule: true, default: reactStub, ...reactStub }; + if (request === 'lucide-react') return lucideStub; + if (request === './quicklink-icons') { + return { renderQuickLinkIconGlyph: () => null }; + } + if (request.startsWith('../icons/')) { + return { __esModule: true, default: makeComponent(path.basename(request)) }; + } + return null; +} + +function loadTsModule(filePath) { + const resolvedPath = path.resolve(ROOT, filePath); + if (moduleCache.has(resolvedPath)) return moduleCache.get(resolvedPath).exports; + + const source = fs.readFileSync(resolvedPath, 'utf8'); + const transpiled = ts.transpileModule(source, { + compilerOptions: { + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2022, + jsx: ts.JsxEmit.React, + esModuleInterop: true, + importsNotUsedAsValues: ts.ImportsNotUsedAsValues.Remove, + }, + fileName: resolvedPath, + }); + + const module = { exports: {} }; + moduleCache.set(resolvedPath, module); + + const localRequire = (request) => { + const stubbed = getStubbedModule(request); + if (stubbed) return stubbed; + + if (request.startsWith('.')) { + const candidate = path.resolve(path.dirname(resolvedPath), request); + for (const suffix of ['', '.ts', '.tsx', '.js', '.jsx', '.json', '/index.ts', '/index.tsx']) { + const nextPath = `${candidate}${suffix}`; + if (!fs.existsSync(nextPath) || !fs.statSync(nextPath).isFile()) continue; + if (nextPath.endsWith('.svg')) return ''; + if (nextPath.endsWith('.ts') || nextPath.endsWith('.tsx')) return loadTsModule(nextPath); + return require(nextPath); + } + } + + return require(request); + }; + + const sandbox = { + module, + exports: module.exports, + require: localRequire, + console, + URL, + Date, + Math, + String, + Number, + Set, + Map, + Object, + Array, + RegExp, + }; + + vm.runInNewContext(transpiled.outputText, sandbox, { filename: resolvedPath }); + return module.exports; +} + +const commandHelpers = loadTsModule('src/renderer/src/utils/command-helpers.tsx'); +const ranking = loadTsModule('src/renderer/src/utils/root-search-ranking.ts'); + +const { createRootCommandScoreIndex, filterCommands, rankCommands, rankCommandsWithIndex } = commandHelpers; +const { scoreRootSearchFields } = ranking; + +const COMMAND_COUNT = Number(process.env.SUPERCMD_ROOT_SEARCH_PERF_COMMANDS || 2000); +const ITERATIONS = Number(process.env.SUPERCMD_ROOT_SEARCH_PERF_ITERATIONS || 1); +const WARMUP_ITERATIONS = Number(process.env.SUPERCMD_ROOT_SEARCH_PERF_WARMUPS || 0); + +const QUERIES = [ + 'notes', + 'clip', + 'window', + 'project alpha', + 'deploy', + 'timer', + 'gh', + 'cmd 42', +]; + +const WORDS = [ + 'Notes', + 'Clipboard', + 'Window', + 'Browser', + 'Settings', + 'Canvas', + 'Timer', + 'Schedule', + 'Project', + 'Deploy', + 'Debug', + 'GitHub', + 'Snippet', + 'Search', + 'Memory', + 'Automation', +]; + +function normalizeSearchText(value) { + return String(value || '') + .normalize('NFKD') + .toLowerCase() + .replace(/[^\p{L}\p{N}]+/gu, ' ') + .trim(); +} + +function makeCommands(count) { + const commands = []; + const aliases = {}; + + for (let i = 0; i < count; i += 1) { + const primary = WORDS[i % WORDS.length]; + const secondary = WORDS[(i * 7 + 3) % WORDS.length]; + const group = i % 4 === 0 ? 'extension' : i % 4 === 1 ? 'script' : i % 4 === 2 ? 'app' : 'system'; + const command = { + id: `synthetic-command-${i}`, + title: `${primary} ${secondary} Command ${i}`, + subtitle: `${secondary} tools for Project Alpha workspace ${i % 97}`, + category: group, + path: group === 'extension' ? `extension-${i % 80}/command-${i}` : undefined, + keywords: [ + primary, + secondary, + `cmd ${i}`, + i % 9 === 0 ? 'project alpha' : 'workspace', + i % 11 === 0 ? 'gh github repo' : 'local action', + ], + alwaysOnTop: i % 997 === 0, + }; + commands.push(command); + + if (i % 5 === 0) aliases[command.id] = `${primary.toLowerCase()}-${i}`; + if (i % 37 === 0) aliases[command.id] = 'gh'; + } + + return { commands, aliases }; +} + +function rootCommandFields(command, alias) { + return [ + { value: command.title, kind: 'label', weight: 1 }, + { value: alias, kind: 'alias', weight: 1.08 }, + { value: command.subtitle, kind: 'description', weight: 0.74 }, + ...(command.keywords || []).map((keyword) => ({ value: keyword, kind: 'description', weight: 0.68 })), + ]; +} + +function legacyRankCommandFields(command, alias) { + return [ + { value: command.title, kind: 'label', weight: 1 }, + { value: alias, kind: 'alias', weight: 1.06 }, + { value: command.subtitle, kind: 'description', weight: 0.74 }, + ...(command.keywords || []).map((keyword) => ({ value: keyword, kind: 'description', weight: 0.7 })), + ]; +} + +function legacyRootCommandMatches(commands, query, aliases) { + const normalizedQuery = normalizeSearchText(query); + const ranked = commands + .map((command) => { + const alias = aliases[command.id] || ''; + const firstPass = scoreRootSearchFields(query, legacyRankCommandFields(command, alias)); + if (!firstPass.matched) return null; + return { + command, + score: firstPass.matchScore, + hasExactAliasMatch: Boolean(alias) && normalizeSearchText(alias) === normalizedQuery, + }; + }) + .filter(Boolean) + .sort((a, b) => { + if (a.hasExactAliasMatch !== b.hasExactAliasMatch) { + return Number(b.hasExactAliasMatch) - Number(a.hasExactAliasMatch); + } + if (a.command.alwaysOnTop !== b.command.alwaysOnTop) return a.command.alwaysOnTop ? -1 : 1; + if (b.score !== a.score) return b.score - a.score; + return a.command.title.localeCompare(b.command.title); + }); + + return ranked + .map(({ command }) => { + const scored = scoreRootSearchFields(query, rootCommandFields(command, aliases[command.id] || '')); + assert.equal(scored.matched, true, `${command.id} lost its second-pass match for ${query}`); + return { + id: command.id, + matchKind: scored.matchKind, + matchScore: scored.matchScore, + }; + }); +} + +function optimizedRootCommandMatches(commands, query, aliases) { + return rankCommands(commands, query, aliases) + .map((entry) => { + if (typeof entry.matchKind === 'string' && typeof entry.matchScore === 'number') { + return { + id: entry.command.id, + matchKind: entry.matchKind, + matchScore: entry.matchScore, + }; + } + + const scored = scoreRootSearchFields(query, rootCommandFields(entry.command, aliases[entry.command.id] || '')); + assert.equal(scored.matched, true, `${entry.command.id} lost its optimized fallback match for ${query}`); + return { + id: entry.command.id, + matchKind: scored.matchKind, + matchScore: scored.matchScore, + }; + }); +} + +function indexedRootCommandMatches(index, query) { + return rankCommandsWithIndex(index, query) + .map((entry) => ({ + id: entry.command.id, + matchKind: entry.matchKind, + matchScore: entry.matchScore, + })); +} + +function signature(matches) { + return matches + .map((match) => `${match.id}:${match.matchKind}:${match.matchScore}`) + .sort(); +} + +function assertSameRootCommandMatches(commands, aliases) { + const index = createRootCommandScoreIndex(commands, aliases); + for (const query of QUERIES) { + const legacySignature = signature(legacyRootCommandMatches(commands, query, aliases)); + assert.deepEqual( + signature(optimizedRootCommandMatches(commands, query, aliases)), + legacySignature, + `root command matches changed for query "${query}"` + ); + assert.deepEqual( + signature(indexedRootCommandMatches(index, query)), + legacySignature, + `indexed root command matches changed for query "${query}"` + ); + } +} + +function timeRun(fn) { + const start = performance.now(); + let total = 0; + for (const query of QUERIES) { + total += fn(query); + } + return { duration: performance.now() - start, total }; +} + +function measure(label, fn) { + for (let i = 0; i < WARMUP_ITERATIONS; i += 1) timeRun(fn); + + const samples = []; + let total = 0; + for (let i = 0; i < ITERATIONS; i += 1) { + const result = timeRun(fn); + samples.push(result.duration); + total = result.total; + } + + samples.sort((a, b) => a - b); + const median = samples[Math.floor(samples.length / 2)]; + const min = samples[0]; + const max = samples[samples.length - 1]; + + return { label, median, min, max, total }; +} + +function measureSingle(label, fn) { + for (let i = 0; i < WARMUP_ITERATIONS; i += 1) fn(); + + const samples = []; + let total = 0; + for (let i = 0; i < ITERATIONS; i += 1) { + const start = performance.now(); + total = fn(); + samples.push(performance.now() - start); + } + + samples.sort((a, b) => a - b); + const median = samples[Math.floor(samples.length / 2)]; + const min = samples[0]; + const max = samples[samples.length - 1]; + + return { label, median, min, max, total }; +} + +const { commands, aliases } = makeCommands(COMMAND_COUNT); + +assertSameRootCommandMatches(commands, aliases); +const commandScoreIndex = createRootCommandScoreIndex(commands, aliases); + +const legacy = measure('legacy filter + two-pass command scoring', (query) => { + const filtered = filterCommands(commands, query, aliases); + const matches = legacyRootCommandMatches(commands, query, aliases); + return filtered.length + matches.length; +}); + +const optimized = measure('optimized command scoring path', (query) => { + const matches = optimizedRootCommandMatches(commands, query, aliases); + return matches.length; +}); + +const indexed = measure('indexed command scoring path', (query) => { + const matches = indexedRootCommandMatches(commandScoreIndex, query); + return matches.length; +}); + +const compile = measureSingle('command scoring index compile', () => { + return createRootCommandScoreIndex(commands, aliases).entries.length; +}); + +const optimizedSpeedup = legacy.median > 0 ? legacy.median / optimized.median : 0; +const indexedSpeedup = legacy.median > 0 ? legacy.median / indexed.median : 0; +const indexedVsOptimizedSpeedup = optimized.median > 0 ? optimized.median / indexed.median : 0; + +console.log('Root search perf harness'); +console.log(`commands=${COMMAND_COUNT} queries=${QUERIES.length} iterations=${ITERATIONS} warmups=${WARMUP_ITERATIONS}`); +console.log(`${legacy.label}: median=${legacy.median.toFixed(2)}ms min=${legacy.min.toFixed(2)}ms max=${legacy.max.toFixed(2)}ms total=${legacy.total}`); +console.log(`${optimized.label}: median=${optimized.median.toFixed(2)}ms min=${optimized.min.toFixed(2)}ms max=${optimized.max.toFixed(2)}ms total=${optimized.total}`); +console.log(`${indexed.label}: median=${indexed.median.toFixed(2)}ms min=${indexed.min.toFixed(2)}ms max=${indexed.max.toFixed(2)}ms total=${indexed.total}`); +console.log(`${compile.label}: median=${compile.median.toFixed(2)}ms min=${compile.min.toFixed(2)}ms max=${compile.max.toFixed(2)}ms total=${compile.total}`); +console.log(`legacy-vs-optimized-speedup=${optimizedSpeedup.toFixed(2)}x`); +console.log(`legacy-vs-indexed-speedup=${indexedSpeedup.toFixed(2)}x`); +console.log(`optimized-vs-indexed-speedup=${indexedVsOptimizedSpeedup.toFixed(2)}x`); From 813b45767beb6459ce2d3e6f8b9fc55acc4313e8 Mon Sep 17 00:00:00 2001 From: Yannic Charlon <52761674+JustYannicc@users.noreply.github.com> Date: Sat, 4 Jul 2026 12:47:36 +0200 Subject: [PATCH 04/13] fix(validation): typecheck renderer in CI --- .github/workflows/project-checks.yml | 3 +++ package.json | 1 + 2 files changed, 4 insertions(+) diff --git a/.github/workflows/project-checks.yml b/.github/workflows/project-checks.yml index 41044d27..536dce79 100644 --- a/.github/workflows/project-checks.yml +++ b/.github/workflows/project-checks.yml @@ -51,5 +51,8 @@ jobs: - name: Build main process run: npm run build:main + - name: Typecheck renderer + run: npm run typecheck:renderer + - name: Build renderer run: npm run build:renderer diff --git a/package.json b/package.json index 578fd62e..75b50ded 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "start:electron": "wait-on dist/main/main.js && cross-env NODE_ENV=development SUPERCMD_OPEN_DEVTOOLS_ON_STARTUP=1 electron .", "build": "npm run build:main && npm run build:renderer && npm run build:native", "build:main": "tsc -p tsconfig.main.json && cp src/main/emoji-data.json dist/main/emoji-data.json", + "typecheck:renderer": "tsc -p tsconfig.renderer.json --noEmit --pretty false", "build:renderer": "vite build", "check:i18n": "node scripts/check-i18n.mjs", "test": "node --test 'scripts/test-*.mjs'", From b4d6edd43c24e69e87b09578d753c79a331e3606 Mon Sep 17 00:00:00 2001 From: Yannic Charlon <52761674+JustYannicc@users.noreply.github.com> Date: Sat, 4 Jul 2026 12:53:31 +0200 Subject: [PATCH 05/13] perf(validation): skip warm native rebuilds --- scripts/build-native.mjs | 94 +++++++++++++++++++++++++--- scripts/build-parakeet.mjs | 2 + scripts/build-soulver-calculator.mjs | 2 + scripts/build-whispercpp.mjs | 86 ++++++++++++++++++++++--- 4 files changed, 165 insertions(+), 19 deletions(-) diff --git a/scripts/build-native.mjs b/scripts/build-native.mjs index 35f3172f..eb02015e 100644 --- a/scripts/build-native.mjs +++ b/scripts/build-native.mjs @@ -1,19 +1,78 @@ #!/usr/bin/env node import { execSync } from 'child_process'; -import { mkdirSync } from 'fs'; +import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs'; import { createRequire } from 'module'; +import path from 'path'; +import { fileURLToPath } from 'url'; const require = createRequire(import.meta.url); +const __filename = fileURLToPath(import.meta.url); mkdirSync('dist/native', { recursive: true }); const electronVersion = require('../node_modules/electron/package.json').version; const arch = process.arch; +const stampVersion = 1; function run(cmd) { execSync(cmd, { stdio: 'inherit' }); } +function readStamp(stampPath) { + try { + return JSON.parse(readFileSync(stampPath, 'utf8')); + } catch { + return null; + } +} + +function inputState(filePath) { + if (!existsSync(filePath)) return null; + const stats = statSync(filePath); + return { + path: filePath, + mtimeMs: stats.mtimeMs, + size: stats.size, + }; +} + +function isUpToDate(out, stampPath, stamp) { + if (!existsSync(out)) return false; + const currentInputs = stamp.inputs.map(inputState); + if (currentInputs.some((input) => input === null)) return false; + + const previous = readStamp(stampPath); + if (JSON.stringify(previous) !== JSON.stringify({ ...stamp, inputState: currentInputs })) { + return false; + } + + const outputMtime = statSync(out).mtimeMs; + return currentInputs.every((input) => input.mtimeMs <= outputMtime); +} + +function writeStamp(stampPath, stamp) { + const currentInputs = stamp.inputs.map(inputState); + writeFileSync(stampPath, JSON.stringify({ ...stamp, inputState: currentInputs }, null, 2)); +} + +function buildIfStale({ label, out, inputs, command }) { + const stampPath = `${out}.stamp.json`; + const stamp = { + version: stampVersion, + label, + command, + inputs, + }; + + if (isUpToDate(out, stampPath, stamp)) { + console.log(`[native] ${path.basename(out)} is up to date, skipping rebuild`); + return; + } + + run(command); + writeStamp(stampPath, stamp); +} + const swift = [ ['dist/native/get-selected-text', 'src/native/get-selected-text.swift', '-framework Foundation -framework ApplicationServices -framework AppKit'], @@ -49,17 +108,34 @@ const swift = [ ]; for (const [out, src, frameworks] of swift) { - run(`swiftc -O -o ${out} ${src} ${frameworks}`); + const inputs = [...src.split(' '), __filename]; + buildIfStale({ + label: path.basename(out), + out, + inputs, + command: `swiftc -O -o ${out} ${src} ${frameworks}`, + }); } // Build native Node addon (native_helpers.node) -run( - `cd src/native/native-helpers-addon && ` + - `HOME=~/.electron-gyp npx node-gyp rebuild ` + - `--target=${electronVersion} --arch=${arch} ` + - `--dist-url=https://electronjs.org/headers && ` + - `cp build/Release/native_helpers.node ../../../dist/native/native_helpers.node` -); +buildIfStale({ + label: 'native_helpers.node', + out: 'dist/native/native_helpers.node', + inputs: [ + 'src/native/native-helpers-addon/binding.gyp', + 'src/native/native-helpers-addon/native_helpers.mm', + 'package.json', + 'package-lock.json', + 'node_modules/electron/package.json', + __filename, + ], + command: + `cd src/native/native-helpers-addon && ` + + `HOME=~/.electron-gyp npx node-gyp rebuild ` + + `--target=${electronVersion} --arch=${arch} ` + + `--dist-url=https://electronjs.org/headers && ` + + `cp build/Release/native_helpers.node ../../../dist/native/native_helpers.node`, +}); run('node scripts/build-whispercpp.mjs'); run('node scripts/build-parakeet.mjs'); diff --git a/scripts/build-parakeet.mjs b/scripts/build-parakeet.mjs index 71fd8ff3..d837b2af 100644 --- a/scripts/build-parakeet.mjs +++ b/scripts/build-parakeet.mjs @@ -14,8 +14,10 @@ const distNativeDir = path.join(repoRoot, 'dist', 'native'); const binaryDest = path.join(distNativeDir, 'parakeet-transcriber'); const sourceFiles = [ + __filename, path.join(packageDir, 'Sources', 'main.swift'), path.join(packageDir, 'Package.swift'), + path.join(packageDir, 'Package.resolved'), ]; function needsRebuild() { diff --git a/scripts/build-soulver-calculator.mjs b/scripts/build-soulver-calculator.mjs index e02d8a4d..75f7e75f 100644 --- a/scripts/build-soulver-calculator.mjs +++ b/scripts/build-soulver-calculator.mjs @@ -15,8 +15,10 @@ const binaryDest = path.join(outDir, 'soulver-calculator'); const frameworkDest = path.join(outDir, 'SoulverCore.framework'); const sourceFiles = [ + __filename, path.join(packageDir, 'Sources', 'main.swift'), path.join(packageDir, 'Package.swift'), + path.join(packageDir, 'Package.resolved'), ]; function needsRebuild() { diff --git a/scripts/build-whispercpp.mjs b/scripts/build-whispercpp.mjs index cad84f2c..81a40e61 100644 --- a/scripts/build-whispercpp.mjs +++ b/scripts/build-whispercpp.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'fs'; +import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs'; import { createHash } from 'crypto'; import path from 'path'; import { tmpdir } from 'os'; @@ -23,6 +23,18 @@ const runtimeDir = path.join(distNativeDir, 'whisper-runtime'); const frameworkDir = path.join(runtimeDir, 'whisper.framework'); const transcriberSource = path.join(repoRoot, 'src', 'native', 'whisper-transcriber.swift'); const transcriberBinary = path.join(distNativeDir, 'whisper-transcriber'); +const transcriberStamp = path.join(distNativeDir, 'whisper-transcriber.stamp.json'); +const stampVersion = 1; +const transcriberBuildArgs = [ + '-O', + '-module-cache-path', '/supercmd-swift-module-cache', + '-F', runtimeDir, + '-framework', 'whisper', + '-Xlinker', '-rpath', + '-Xlinker', '@executable_path/whisper-runtime', + '-o', transcriberBinary, + transcriberSource, +]; function run(command, args, options = {}) { const result = spawnSync(command, args, { @@ -85,17 +97,66 @@ function buildTranscriber() { console.log('[whisper.cpp] Building whisper-transcriber'); run('swiftc', [ - '-O', - '-module-cache-path', moduleCacheDir, - '-F', runtimeDir, - '-framework', 'whisper', - '-Xlinker', '-rpath', - '-Xlinker', '@executable_path/whisper-runtime', - '-o', transcriberBinary, - transcriberSource, + ...transcriberBuildArgs.map((arg) => arg === '/supercmd-swift-module-cache' ? moduleCacheDir : arg), ]); } +function newestMtimeMs(filePath) { + const stats = lstatSync(filePath); + if (stats.isSymbolicLink()) return stats.mtimeMs; + if (!stats.isDirectory()) return stats.mtimeMs; + + return readdirSync(filePath).reduce((newest, entry) => { + return Math.max(newest, newestMtimeMs(path.join(filePath, entry))); + }, stats.mtimeMs); +} + +function transcriberStampPayload() { + return { + version: stampVersion, + whisperVersion, + frameworkUrl, + frameworkSha256, + command: ['swiftc', ...transcriberBuildArgs], + source: { + path: transcriberSource, + mtimeMs: statSync(transcriberSource).mtimeMs, + size: statSync(transcriberSource).size, + }, + script: { + path: __filename, + mtimeMs: statSync(__filename).mtimeMs, + size: statSync(__filename).size, + }, + framework: { + path: frameworkDir, + mtimeMs: newestMtimeMs(frameworkDir), + }, + }; +} + +function readStamp() { + try { + return JSON.parse(readFileSync(transcriberStamp, 'utf8')); + } catch { + return null; + } +} + +function transcriberIsUpToDate() { + if (!existsSync(transcriberBinary) || !existsSync(transcriberSource) || !existsSync(frameworkDir)) return false; + + const payload = transcriberStampPayload(); + if (JSON.stringify(readStamp()) !== JSON.stringify(payload)) return false; + + const binaryMtime = statSync(transcriberBinary).mtimeMs; + return payload.source.mtimeMs <= binaryMtime && payload.framework.mtimeMs <= binaryMtime; +} + +function writeTranscriberStamp() { + writeFileSync(transcriberStamp, JSON.stringify(transcriberStampPayload(), null, 2)); +} + try { // Verify the framework actually has the whisper module, not just an empty directory const moduleMapPath = path.join(frameworkDir, 'Modules', 'module.modulemap'); @@ -104,7 +165,12 @@ try { } else { console.log('[whisper.cpp] Using existing macOS framework'); } - buildTranscriber(); + if (transcriberIsUpToDate()) { + console.log('[whisper.cpp] whisper-transcriber is up to date, skipping rebuild'); + } else { + buildTranscriber(); + writeTranscriberStamp(); + } console.log('[whisper.cpp] Ready'); } catch (error) { console.error('[whisper.cpp] Build failed:', error instanceof Error ? error.message : error); From 500ad65a8355bd075713e91bb05e094a9be5d84a Mon Sep 17 00:00:00 2001 From: Yannic Charlon <52761674+JustYannicc@users.noreply.github.com> Date: Sat, 4 Jul 2026 13:07:46 +0200 Subject: [PATCH 06/13] perf(validation): support indexed root search harness --- src/renderer/src/utils/command-helpers.tsx | 128 ++++++++++++---- src/renderer/src/utils/root-search-ranking.ts | 141 +++++++++++++----- 2 files changed, 203 insertions(+), 66 deletions(-) diff --git a/src/renderer/src/utils/command-helpers.tsx b/src/renderer/src/utils/command-helpers.tsx index dfa5bcbc..bb13bb7d 100644 --- a/src/renderer/src/utils/command-helpers.tsx +++ b/src/renderer/src/utils/command-helpers.tsx @@ -27,7 +27,14 @@ import IconPen from '../icons/Pen'; import IconMagicWand from '../icons/MagicWand'; import { formatShortcutForDisplay } from './hyper-key'; import { renderQuickLinkIconGlyph } from './quicklink-icons'; -import { scoreRootSearchFields } from './root-search-ranking'; +import { + precompileRootSearchQuery, + precompileRootSearchScoringFields, + scorePrecompiledRootSearchFields, + type MatchKind, + type PrecompiledRootSearchScoringField, + type RootSearchScoringField, +} from './root-search-ranking'; import { getTranslitVariant } from './transliterate'; export interface LauncherAction { @@ -192,8 +199,57 @@ type SearchCandidate = { export type RankedCommand = { command: CommandInfo; score: number; + matchKind: MatchKind; + matchScore: number; }; +export type IndexedRankedCommand = RankedCommand & { + matchKind: MatchKind; + matchScore: number; +}; + +export type RootCommandScoreIndexEntry = { + command: CommandInfo; + rankingFields: PrecompiledRootSearchScoringField[]; + scoringFields: PrecompiledRootSearchScoringField[]; + normalizedAlias: string; +}; + +export type RootCommandScoreIndex = { + entries: RootCommandScoreIndexEntry[]; +}; + +function getRootSearchCommandRankingFields(command: CommandInfo, alias: string): RootSearchScoringField[] { + return [ + { value: command.title, kind: 'label', weight: 1 }, + { value: alias, kind: 'alias', weight: 1.06 }, + { value: command.subtitle, kind: 'description', weight: 0.74 }, + ...(command.keywords || []).map((keyword) => ({ value: keyword, kind: 'description' as const, weight: 0.7 })), + ]; +} + +export function createRootCommandScoreIndex( + commands: CommandInfo[], + aliasLookup: Record = {} +): RootCommandScoreIndex { + return { + entries: commands.map((command) => { + const alias = aliasLookup[command.id] || ''; + const rankingFields = precompileRootSearchScoringFields(getRootSearchCommandRankingFields(command, alias)); + const scoringFields = rankingFields.map((field, index) => { + const scoringWeight = index === 1 ? 1.08 : index >= 3 ? 0.68 : field.weight; + return scoringWeight === field.weight ? field : { ...field, weight: scoringWeight }; + }); + return { + command, + rankingFields, + scoringFields, + normalizedAlias: normalizeSearchText(alias), + }; + }), + }; +} + function bestTermScore(term: string, candidates: SearchCandidate[]): number { let best = 0; for (const candidate of candidates) { @@ -339,37 +395,59 @@ export function filterCommands( return [...matchedTop, ...matchedRest]; } -export function rankCommands( - commands: CommandInfo[], - query: string, - aliasLookup: Record = {} -): RankedCommand[] { +type RankedCommandSortEntry = IndexedRankedCommand & { + hasExactAliasMatch: boolean; +}; + +export function rankCommandsWithIndex(index: RootCommandScoreIndex, query: string): IndexedRankedCommand[] { const normalizedQuery = normalizeSearchText(query); if (!normalizedQuery) { - return commands.map((command) => ({ command, score: command.alwaysOnTop ? Number.MAX_SAFE_INTEGER : 0 })); - } - - return commands - .map((command): RankedCommand | null => { - const alias = aliasLookup[command.id] || ''; - const scored = scoreRootSearchFields(query, [ - { value: command.title, kind: 'label', weight: 1 }, - { value: alias, kind: 'alias', weight: 1.06 }, - { value: command.subtitle, kind: 'description', weight: 0.74 }, - ...(command.keywords || []).map((keyword) => ({ value: keyword, kind: 'description' as const, weight: 0.7 })), - ]); - if (!scored.matched) return null; - return { command, score: scored.matchScore }; + return index.entries.map(({ command }) => ({ + command, + score: command.alwaysOnTop ? Number.MAX_SAFE_INTEGER : 0, + matchKind: 'exact', + matchScore: 0, + })); + } + + const compiledQuery = precompileRootSearchQuery(query); + + return index.entries + .map((entry): RankedCommandSortEntry | null => { + const ranked = scorePrecompiledRootSearchFields(compiledQuery, entry.rankingFields); + if (!ranked.matched) return null; + const scored = scorePrecompiledRootSearchFields(compiledQuery, entry.scoringFields); + return { + command: entry.command, + score: ranked.matchScore, + matchKind: scored.matched ? scored.matchKind : ranked.matchKind, + matchScore: scored.matched ? scored.matchScore : ranked.matchScore, + hasExactAliasMatch: entry.normalizedAlias === normalizedQuery, + }; }) - .filter((entry): entry is RankedCommand => entry !== null) + .filter((entry): entry is RankedCommandSortEntry => entry !== null) .sort((a, b) => { - const aAlias = normalizeSearchText(aliasLookup[a.command.id] || '') === normalizedQuery; - const bAlias = normalizeSearchText(aliasLookup[b.command.id] || '') === normalizedQuery; - if (aAlias !== bAlias) return Number(bAlias) - Number(aAlias); + if (a.hasExactAliasMatch !== b.hasExactAliasMatch) { + return Number(b.hasExactAliasMatch) - Number(a.hasExactAliasMatch); + } if (a.command.alwaysOnTop !== b.command.alwaysOnTop) return a.command.alwaysOnTop ? -1 : 1; if (b.score !== a.score) return b.score - a.score; return a.command.title.localeCompare(b.command.title); - }); + }) + .map(({ command, score, matchKind, matchScore }) => ({ + command, + score, + matchKind, + matchScore, + })); +} + +export function rankCommands( + commands: CommandInfo[], + query: string, + aliasLookup: Record = {} +): RankedCommand[] { + return rankCommandsWithIndex(createRootCommandScoreIndex(commands, aliasLookup), query); } /** diff --git a/src/renderer/src/utils/root-search-ranking.ts b/src/renderer/src/utils/root-search-ranking.ts index 6eec2570..2d4bacfe 100644 --- a/src/renderer/src/utils/root-search-ranking.ts +++ b/src/renderer/src/utils/root-search-ranking.ts @@ -90,15 +90,32 @@ export type RootSearchScoreResult = { matchScore: number; }; +export type PrecompiledRootSearchQueryTerm = { + normalized: string; + compact: string; +}; + +export type PrecompiledRootSearchQuery = { + fullQuery: string; + terms: PrecompiledRootSearchQueryTerm[]; +}; + +export type PrecompiledRootSearchScoringField = { + raw: string; + normalized: string; + compact: string; + tokens: string[]; + boundaryInitials: string; + kind: RootSearchFieldKind; + weight: number; + isSecondaryField: boolean; + secondaryKind: MatchKind; +}; + const SEARCH_SEPARATOR_REGEX = /[^\p{L}\p{N}]+/gu; const COMBINING_MARK_REGEX = /\p{M}/gu; const DAY = 24 * 60 * 60 * 1000; -// Build liveness stamp — confirms the running renderer has the internal>browser -// precedence fix. Grep the bundle (dist/renderer/assets/*.js) for this string, -// or look for it in the DevTools console at startup. Remove once verified. -try { console.info('[SC-RANK build 2026-06-19c internal>browser precedence ACTIVE]'); } catch {} - export const ROOT_SEARCH_RESULTS_LIMIT = 8; export const ROOT_SEARCH_PROMOTION_SCORE = 700; @@ -147,9 +164,13 @@ export function compactRootSearchText(value: string): string { return normalizeRootSearchText(value).replace(SEARCH_SEPARATOR_REGEX, '').replace(/\s+/g, ''); } +function tokenizeNormalizedRootSearchText(normalized: string): string[] { + return normalized ? normalized.split(/\s+/).filter(Boolean) : []; +} + export function tokenizeRootSearchQuery(value: string): string[] { const normalized = normalizeRootSearchText(value); - return normalized ? normalized.split(/\s+/).filter(Boolean) : []; + return tokenizeNormalizedRootSearchText(normalized); } export function normalizeRootSearchStableValue(value: string): string { @@ -183,86 +204,117 @@ function isSubsequenceMatch(needle: string, haystack: string): boolean { return needleIndex === needle.length; } -function hasBoundaryFuzzyMatch(term: string, value: string): boolean { - if (!term || !value) return false; - const normalized = normalizeRootSearchText(value); - if (normalized.split(/\s+/).some((token) => token.startsWith(term))) return true; - const compactTerm = compactRootSearchText(term); +function getRootSearchBoundaryInitials(value: string): string { const boundaryChars = String(value || '').match(/[A-Z]?[a-z]+|[A-Z]+(?![a-z])|\d+/g) || []; const camelInitials = boundaryChars.map((part) => part[0]).join(''); - return Boolean(compactTerm.length >= 2 && compactRootSearchText(camelInitials).startsWith(compactTerm)); + return compactRootSearchText(camelInitials); +} + +export function precompileRootSearchQuery(query: string): PrecompiledRootSearchQuery { + const fullQuery = normalizeRootSearchText(query); + return { + fullQuery, + terms: tokenizeNormalizedRootSearchText(fullQuery).map((term) => ({ + normalized: normalizeRootSearchText(term), + compact: compactRootSearchText(term), + })), + }; } -function scoreSingleField(term: string, fullQuery: string, field: RootSearchScoringField): RootSearchScoreResult { +function getRootSearchFieldWeight(field: RootSearchScoringField): number { + return field.weight ?? (field.kind === 'label' ? 1 : field.kind === 'alias' || field.kind === 'nickname' ? 1.04 : 0.72); +} + +function getSecondaryRootSearchMatchKind(field: RootSearchScoringField): MatchKind { + return field.kind === 'url' ? 'url' : field.kind === 'path' ? 'path' : 'description'; +} + +export function precompileRootSearchScoringField(field: RootSearchScoringField): PrecompiledRootSearchScoringField { const raw = String(field.value || ''); const normalized = normalizeRootSearchText(raw); - if (!normalized) return { matched: false, matchKind: 'subsequence', matchScore: 0 }; - - const normalizedTerm = normalizeRootSearchText(term); - const compactField = compactRootSearchText(raw); - const compactTerm = compactRootSearchText(term); - const tokens = normalized.split(/\s+/).filter(Boolean); const isSecondaryField = field.kind === 'description' || field.kind === 'path' || field.kind === 'url'; - const secondaryKind: MatchKind = field.kind === 'url' ? 'url' : field.kind === 'path' ? 'path' : 'description'; - const weight = field.weight ?? (field.kind === 'label' ? 1 : field.kind === 'alias' || field.kind === 'nickname' ? 1.04 : 0.72); + return { + raw, + normalized, + compact: normalized ? compactRootSearchText(raw) : '', + tokens: tokenizeNormalizedRootSearchText(normalized), + boundaryInitials: raw ? getRootSearchBoundaryInitials(raw) : '', + kind: field.kind, + weight: getRootSearchFieldWeight(field), + isSecondaryField, + secondaryKind: getSecondaryRootSearchMatchKind(field), + }; +} + +export function precompileRootSearchScoringFields(fields: RootSearchScoringField[]): PrecompiledRootSearchScoringField[] { + return fields.map(precompileRootSearchScoringField); +} + +function scorePrecompiledSingleField( + term: PrecompiledRootSearchQueryTerm, + fullQuery: string, + field: PrecompiledRootSearchScoringField +): RootSearchScoreResult { + if (!field.normalized) return { matched: false, matchKind: 'subsequence', matchScore: 0 }; let kind: MatchKind | null = null; let baseScore = 0; - if (normalized === fullQuery || normalized === normalizedTerm) { + if (field.normalized === fullQuery || field.normalized === term.normalized) { kind = field.kind === 'alias' ? 'alias-exact' : field.kind === 'nickname' ? 'nickname-exact' : 'exact'; baseScore = 1000; - } else if (normalized.startsWith(normalizedTerm)) { + } else if (field.normalized.startsWith(term.normalized)) { kind = 'prefix'; baseScore = 900; - } else if (tokens.some((token) => token.startsWith(normalizedTerm))) { + } else if (field.tokens.some((token) => token.startsWith(term.normalized))) { kind = 'token-prefix'; baseScore = 780; - } else if (compactTerm && compactField.startsWith(compactTerm)) { + } else if (term.compact && field.compact.startsWith(term.compact)) { kind = 'compact-prefix'; baseScore = 740; - } else if (hasBoundaryFuzzyMatch(normalizedTerm, raw)) { + } else if (term.compact.length >= 2 && field.boundaryInitials.startsWith(term.compact)) { kind = 'word-boundary-fuzzy'; - const compactness = Math.max(0, 1 - Math.max(0, compactField.length - compactTerm.length) / 24); + const compactness = Math.max(0, 1 - Math.max(0, field.compact.length - term.compact.length) / 24); baseScore = 620 + Math.round(compactness * 110); - } else if (normalizedTerm.length >= 2 && normalized.includes(normalizedTerm)) { + } else if (term.normalized.length >= 2 && field.normalized.includes(term.normalized)) { kind = 'contains'; baseScore = 560; - } else if (compactTerm.length >= 2 && isSubsequenceMatch(compactTerm, compactField)) { + } else if (term.compact.length >= 2 && isSubsequenceMatch(term.compact, field.compact)) { kind = 'subsequence'; - const density = compactTerm.length / Math.max(compactField.length, compactTerm.length); + const density = term.compact.length / Math.max(field.compact.length, term.compact.length); baseScore = 380 + Math.round(Math.min(140, density * 170)); } if (!kind || baseScore <= 0) return { matched: false, matchKind: 'subsequence', matchScore: 0 }; - if (isSecondaryField && baseScore < 900) { - kind = secondaryKind; + if (field.isSecondaryField && baseScore < 900) { + kind = field.secondaryKind; baseScore = Math.min(500, Math.max(260, Math.round(baseScore * 0.72))); } const compactnessBoost = kind === 'exact' || kind === 'alias-exact' || kind === 'nickname-exact' || kind === 'prefix' - ? Math.max(0, 36 - Math.max(0, compactField.length - compactTerm.length) * 2) + ? Math.max(0, 36 - Math.max(0, field.compact.length - term.compact.length) * 2) : 0; - const weightedScore = Math.round((baseScore + compactnessBoost) * weight); + const weightedScore = Math.round((baseScore + compactnessBoost) * field.weight); return { matched: true, matchKind: kind, matchScore: weightedScore }; } -export function scoreRootSearchFields(query: string, fields: RootSearchScoringField[]): RootSearchScoreResult { - const fullQuery = normalizeRootSearchText(query); - const queryTerms = tokenizeRootSearchQuery(query); - if (!fullQuery || queryTerms.length === 0) { +export function scorePrecompiledRootSearchFields( + query: PrecompiledRootSearchQuery, + fields: PrecompiledRootSearchScoringField[] +): RootSearchScoreResult { + if (!query.fullQuery || query.terms.length === 0) { return { matched: false, matchKind: 'subsequence', matchScore: 0 }; } let total = 0; let bestKind: MatchKind = 'subsequence'; - for (const term of queryTerms) { + for (const term of query.terms) { let bestForTerm: RootSearchScoreResult | null = null; for (const field of fields) { - const scored = scoreSingleField(term, fullQuery, field); + const scored = scorePrecompiledSingleField(term, query.fullQuery, field); if (!scored.matched) continue; if (!bestForTerm || scored.matchScore > bestForTerm.matchScore) { bestForTerm = scored; @@ -280,10 +332,17 @@ export function scoreRootSearchFields(query: string, fields: RootSearchScoringFi return { matched: true, matchKind: bestKind, - matchScore: Math.round(total / queryTerms.length), + matchScore: Math.round(total / query.terms.length), }; } +export function scoreRootSearchFields(query: string, fields: RootSearchScoringField[]): RootSearchScoreResult { + return scorePrecompiledRootSearchFields( + precompileRootSearchQuery(query), + precompileRootSearchScoringFields(fields) + ); +} + function getFrecencyBoost(stableKey: string, ranking: RootSearchRankingState | undefined, now: number): number { const entry = ranking?.[stableKey]; if (!entry) return 0; From 6c9a9d243368b8bfc1aa6d91aee735af0d92454e Mon Sep 17 00:00:00 2001 From: Yannic Charlon <52761674+JustYannicc@users.noreply.github.com> Date: Fri, 3 Jul 2026 21:30:08 +0200 Subject: [PATCH 07/13] fix(types): make renderer typecheck clean --- src/renderer/src/App.tsx | 2 +- src/renderer/src/CameraExtension.tsx | 2 +- src/renderer/src/QuickLinkManager.tsx | 2 +- src/renderer/src/SnippetManager.tsx | 2 +- src/renderer/src/hooks/useAiChat.ts | 2 +- .../src/hooks/useLauncherCommandModel.ts | 2 +- .../src/hooks/useLauncherKeyboardControls.ts | 1 + src/renderer/src/i18n/runtime.ts | 4 +++- .../raycast-api/action-runtime-overlay.tsx | 3 ++- .../src/raycast-api/context-scope-runtime.ts | 2 +- .../src/raycast-api/detail-runtime.tsx | 4 ++-- .../raycast-api/hooks/use-cached-promise.ts | 4 ++-- .../src/raycast-api/icon-runtime-phosphor.tsx | 2 +- src/renderer/src/raycast-api/index.tsx | 20 ++++++++-------- .../raycast-api/list-runtime-renderers.tsx | 4 ++-- src/renderer/src/raycast-api/list-runtime.tsx | 4 ++-- .../raycast-api/oauth/oauth-service-core.ts | 4 ++++ .../raycast-api/oauth/with-access-token.tsx | 7 +++--- src/renderer/src/settings/AITab.tsx | 7 ++++-- src/renderer/src/settings/StoreTab.tsx | 1 + .../src/types/liquid-glass-react.d.ts | 23 +++++++++++++++++++ src/renderer/src/utils/quicklink-icons.tsx | 11 ++++----- 22 files changed, 73 insertions(+), 40 deletions(-) create mode 100644 src/renderer/src/types/liquid-glass-react.d.ts diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index a49482ba..aeb00cd4 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1076,7 +1076,7 @@ const App: React.FC = () => { const pinToggleForCommand = useCallback( async (command: CommandInfo) => { - console.log('[PIN-TOGGLE] called for command:', command?.id, command?.name); + console.log('[PIN-TOGGLE] called for command:', command?.id, command?.title); const currentPinned = pinnedCommandsRef.current; const exists = currentPinned.includes(command.id); console.log('[PIN-TOGGLE] currentPinned:', currentPinned, 'exists:', exists); diff --git a/src/renderer/src/CameraExtension.tsx b/src/renderer/src/CameraExtension.tsx index 7de6a3f8..02d43a3b 100644 --- a/src/renderer/src/CameraExtension.tsx +++ b/src/renderer/src/CameraExtension.tsx @@ -298,7 +298,7 @@ const CameraExtension: React.FC = ({ onClose }) => { }, 5000); capturePreviewClearTimerRef.current = window.setTimeout(() => { setCapturePreviewDataUrl(null); - setCapturePreviewClearTimerRef.current = null; + capturePreviewClearTimerRef.current = null; }, 5300); setFlashVisible(true); if (flashTimerRef.current != null) { diff --git a/src/renderer/src/QuickLinkManager.tsx b/src/renderer/src/QuickLinkManager.tsx index 2f749757..deaea4c2 100644 --- a/src/renderer/src/QuickLinkManager.tsx +++ b/src/renderer/src/QuickLinkManager.tsx @@ -1286,7 +1286,7 @@ const QuickLinkManager: React.FC = ({ onClose, initialVie const inputRef = useRef(null); const inlineArgumentLaneRef = useRef(null); const inlineArgumentClusterRef = useRef(null); - const inlineDynamicInputRefs = useRef>([]); + const inlineDynamicInputRefs = useRef>([]); const firstDynamicInputRef = useRef(null); const listRef = useRef(null); const itemRefs = useRef<(HTMLDivElement | null)[]>([]); diff --git a/src/renderer/src/SnippetManager.tsx b/src/renderer/src/SnippetManager.tsx index 3e2e8c65..e5ef51dd 100644 --- a/src/renderer/src/SnippetManager.tsx +++ b/src/renderer/src/SnippetManager.tsx @@ -432,7 +432,7 @@ const SnippetManager: React.FC = ({ onClose, initialView }) const inputRef = useRef(null); const inlineArgumentLaneRef = useRef(null); const inlineArgumentClusterRef = useRef(null); - const inlineArgumentInputRefs = useRef>([]); + const inlineArgumentInputRefs = useRef>([]); const firstDynamicInputRef = useRef(null); const listRef = useRef(null); const itemRefs = useRef<(HTMLDivElement | null)[]>([]); diff --git a/src/renderer/src/hooks/useAiChat.ts b/src/renderer/src/hooks/useAiChat.ts index fea32c94..bce0b4a5 100644 --- a/src/renderer/src/hooks/useAiChat.ts +++ b/src/renderer/src/hooks/useAiChat.ts @@ -36,7 +36,7 @@ export interface UseAiChatReturn { setAiQuery: (value: string) => void; aiInputRef: React.RefObject; aiResponseRef: React.RefObject; - setAiAvailable: (value: boolean) => void; + setAiAvailable: React.Dispatch>; conversations: AiConversation[]; activeConversationId: string | null; startAiChat: (searchQuery: string) => void; diff --git a/src/renderer/src/hooks/useLauncherCommandModel.ts b/src/renderer/src/hooks/useLauncherCommandModel.ts index 44ca0fbc..ff6174e3 100644 --- a/src/renderer/src/hooks/useLauncherCommandModel.ts +++ b/src/renderer/src/hooks/useLauncherCommandModel.ts @@ -187,7 +187,7 @@ function getFileDepthPenalty(result: IndexedFileSearchResult): number { type BrowserLauncherProfile = { id?: string; - browserId?: BrowserSearchSource | string; + browserId?: BrowserSearchSource; displayName: string; detectedName?: string; profileId: string; diff --git a/src/renderer/src/hooks/useLauncherKeyboardControls.ts b/src/renderer/src/hooks/useLauncherKeyboardControls.ts index a385562f..e965d9f1 100644 --- a/src/renderer/src/hooks/useLauncherKeyboardControls.ts +++ b/src/renderer/src/hooks/useLauncherKeyboardControls.ts @@ -91,6 +91,7 @@ export type UseLauncherKeyboardControlsOptions = { kind?: CommandInfo['browserResultKind']; url?: string; sourceProfileId?: string; + openInSourceProfile?: boolean; windowId?: string | number; tabId?: string | number; } diff --git a/src/renderer/src/i18n/runtime.ts b/src/renderer/src/i18n/runtime.ts index edf181c9..e64e92dd 100644 --- a/src/renderer/src/i18n/runtime.ts +++ b/src/renderer/src/i18n/runtime.ts @@ -12,7 +12,9 @@ import itMessages from './locales/it.json'; export type SupportedAppLocale = 'en' | 'zh-Hans' | 'zh-Hant' | 'ja' | 'ko' | 'fr' | 'de' | 'es' | 'ru' | 'it'; export type AppLanguageSetting = 'system' | SupportedAppLocale; export type TranslationValues = Record; -type MessageTree = Record; +interface MessageTree { + [key: string]: string | MessageTree; +} export const DEFAULT_APP_LANGUAGE: AppLanguageSetting = 'system'; export const FALLBACK_APP_LOCALE: SupportedAppLocale = 'en'; diff --git a/src/renderer/src/raycast-api/action-runtime-overlay.tsx b/src/renderer/src/raycast-api/action-runtime-overlay.tsx index 02896d10..e623136c 100644 --- a/src/renderer/src/raycast-api/action-runtime-overlay.tsx +++ b/src/renderer/src/raycast-api/action-runtime-overlay.tsx @@ -112,7 +112,8 @@ export function createActionOverlayRuntime(deps: OverlayDeps) { } if (source && typeof source === 'object') { - const variants = [source.light, source.dark].filter((value): value is string => typeof value === 'string' && value.trim().length > 0); + const sourceVariants = source as Record; + const variants = [sourceVariants.light, sourceVariants.dark].filter((value): value is string => typeof value === 'string' && value.trim().length > 0); if (variants.length > 0) { const assetLikeVariants = variants.filter((value) => hasImageExtension(value)); if (assetLikeVariants.length === 0) return true; diff --git a/src/renderer/src/raycast-api/context-scope-runtime.ts b/src/renderer/src/raycast-api/context-scope-runtime.ts index 440b0bc1..410737cc 100644 --- a/src/renderer/src/raycast-api/context-scope-runtime.ts +++ b/src/renderer/src/raycast-api/context-scope-runtime.ts @@ -89,7 +89,7 @@ export function withExtensionContext(ctx: ExtensionContextSnapshot | undefine try { const value = fn(); if (value && typeof (value as any).then === 'function') { - return (value as Promise).finally(restore) as T; + return (value as unknown as Promise).finally(restore) as T; } restore(); return value; diff --git a/src/renderer/src/raycast-api/detail-runtime.tsx b/src/renderer/src/raycast-api/detail-runtime.tsx index 88d513ff..0566bb26 100644 --- a/src/renderer/src/raycast-api/detail-runtime.tsx +++ b/src/renderer/src/raycast-api/detail-runtime.tsx @@ -90,7 +90,7 @@ export function createDetailRuntime(deps: CreateDetailRuntimeDeps) { for (const node of allChildren) { if (React.isValidElement(node)) { - const typeRecord = node.type as Record | null; + const typeRecord = node.type as unknown as Record | null; if (typeRecord?.[DETAIL_METADATA_RUNTIME_MARKER] === true) { metadataNodes.push(node); continue; @@ -290,7 +290,7 @@ export function createDetailRuntime(deps: CreateDetailRuntimeDeps) { ); const MetadataComponent = ({ children }: { children?: React.ReactNode }) =>
{children}
; - (MetadataComponent as Record)[DETAIL_METADATA_RUNTIME_MARKER] = true; + (MetadataComponent as unknown as Record)[DETAIL_METADATA_RUNTIME_MARKER] = true; MetadataComponent.displayName = 'Detail.Metadata'; const Metadata = Object.assign( diff --git a/src/renderer/src/raycast-api/hooks/use-cached-promise.ts b/src/renderer/src/raycast-api/hooks/use-cached-promise.ts index 7d1f4c4e..0f125309 100644 --- a/src/renderer/src/raycast-api/hooks/use-cached-promise.ts +++ b/src/renderer/src/raycast-api/hooks/use-cached-promise.ts @@ -96,7 +96,7 @@ export function useCachedPromise( if (pageNum === 0) { setAccumulatedData(Array.isArray(pageData) ? pageData : []); } else { - setAccumulatedData((prev) => { + setAccumulatedData((prev: unknown) => { const prevArr = Array.isArray(prev) ? prev : []; const newArr = Array.isArray(pageData) ? pageData : []; return [...prevArr, ...newArr]; @@ -123,7 +123,7 @@ export function useCachedPromise( if (pageNum === 0) { setAccumulatedData(Array.isArray(pageData) ? pageData : []); } else { - setAccumulatedData((prev) => { + setAccumulatedData((prev: unknown) => { const prevArr = Array.isArray(prev) ? prev : []; const newArr = Array.isArray(pageData) ? pageData : []; return [...prevArr, ...newArr]; diff --git a/src/renderer/src/raycast-api/icon-runtime-phosphor.tsx b/src/renderer/src/raycast-api/icon-runtime-phosphor.tsx index 2716a240..c7dcf1ab 100644 --- a/src/renderer/src/raycast-api/icon-runtime-phosphor.tsx +++ b/src/renderer/src/raycast-api/icon-runtime-phosphor.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; -import * as Phosphor from '../../../../node_modules/@phosphor-icons/react/dist/index.es.js'; +import * as Phosphor from '@phosphor-icons/react'; import { RAYCAST_ICON_NAMES, RAYCAST_ICON_VALUE_TO_NAME, type RaycastIconName } from './raycast-icon-enum'; type PhosphorIconWeight = 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone'; diff --git a/src/renderer/src/raycast-api/index.tsx b/src/renderer/src/raycast-api/index.tsx index 95940389..f3e2ddf6 100644 --- a/src/renderer/src/raycast-api/index.tsx +++ b/src/renderer/src/raycast-api/index.tsx @@ -434,8 +434,10 @@ export enum ToastStyle { Failure = 'failure', } +type ToastStyleInput = ToastStyle | 'animated' | 'success' | 'failure'; + export class Toast { - static Style = ToastStyle; + static Style: typeof ToastStyle = ToastStyle; private static _activeToast: Toast | null = null; private _title = ''; @@ -483,7 +485,7 @@ export class Toast { return this._style; } - public set style(value: ToastStyle | Toast.Style | string) { + public set style(value: ToastStyleInput | Toast.Style | string) { this._style = this.normalizeStyle(value); this.refresh(); } @@ -506,7 +508,7 @@ export class Toast { this.refresh(); } - private normalizeStyle(value: ToastStyle | Toast.Style | string | undefined): ToastStyle { + private normalizeStyle(value: ToastStyleInput | Toast.Style | string | undefined): ToastStyle { if (value === ToastStyle.Animated || value === Toast.Style.Animated || value === 'animated') { return ToastStyle.Animated; } @@ -894,16 +896,12 @@ export class Toast { // Toast namespace for types (merged with class) export namespace Toast { - export enum Style { - Animated = 'animated', - Success = 'success', - Failure = 'failure', - } + export type Style = ToastStyle; export interface Options { title: string; message?: string; - style?: ToastStyle | Toast.Style; + style?: ToastStyleInput | Toast.Style; primaryAction?: Alert.ActionOptions; secondaryAction?: Alert.ActionOptions; } @@ -924,9 +922,9 @@ function shouldSuppressBenignGitMissingPathToast(options: Toast.Options): boolea } export async function showToast(options: Toast.Options): Promise; -export async function showToast(style: ToastStyle | Toast.Style, title: string, message?: string): Promise; +export async function showToast(style: ToastStyleInput | Toast.Style, title: string, message?: string): Promise; export async function showToast( - optionsOrStyle: Toast.Options | ToastStyle | Toast.Style, + optionsOrStyle: Toast.Options | ToastStyleInput | Toast.Style, title?: string, message?: string ): Promise { diff --git a/src/renderer/src/raycast-api/list-runtime-renderers.tsx b/src/renderer/src/raycast-api/list-runtime-renderers.tsx index 44d20a55..cd14b3b3 100644 --- a/src/renderer/src/raycast-api/list-runtime-renderers.tsx +++ b/src/renderer/src/raycast-api/list-runtime-renderers.tsx @@ -19,7 +19,7 @@ interface ListRendererDeps { renderIcon: (icon: any, className?: string, assetsPath?: string) => React.ReactNode; resolveTintColor: (tintColor?: string) => string | undefined; resolveReadableTintColor: (tintColor?: string, options?: { minContrast?: number }) => string | undefined; - addHexAlpha: (hex: string, alphaHex?: string) => string | null; + addHexAlpha: (hex: string, alphaHex: string) => string | undefined; } export function createListRenderers(deps: ListRendererDeps) { @@ -102,7 +102,7 @@ export function createListRenderers(deps: ListRendererDeps) { {icon &&
{renderIcon(icon, iconClassName, assetsPath)}
}
{primaryText}
{secondaryText && {secondaryText}} - {accessories?.map((accessory, index) => { + {accessories?.map((accessory: NonNullable[number], index: number) => { const accessoryText = typeof accessory?.text === 'string' ? accessory.text : typeof accessory?.text === 'object' ? accessory.text?.value || '' : ''; const accessoryTextColorRaw = typeof accessory?.text === 'object' ? accessory.text?.color : undefined; const tagText = typeof accessory?.tag === 'string' ? accessory.tag : typeof accessory?.tag === 'object' ? accessory.tag?.value || '' : ''; diff --git a/src/renderer/src/raycast-api/list-runtime.tsx b/src/renderer/src/raycast-api/list-runtime.tsx index d7b665bd..c951e63c 100644 --- a/src/renderer/src/raycast-api/list-runtime.tsx +++ b/src/renderer/src/raycast-api/list-runtime.tsx @@ -34,7 +34,7 @@ interface ListRuntimeDeps { renderIcon: (icon: any, className?: string, assetsPath?: string) => React.ReactNode; resolveTintColor: (value?: string) => string | undefined; resolveReadableTintColor: (value?: string, options?: { minContrast?: number }) => string | undefined; - addHexAlpha: (hex: string, alphaHex?: string) => string | null; + addHexAlpha: (hex: string, alphaHex: string) => string | undefined; getExtensionContext: () => { assetsPath: string; extensionDisplayName?: string; @@ -390,7 +390,7 @@ export function createListRuntime(deps: ListRuntimeDeps) { const detailElement = useMemo(() => { if (!rawDetail || !React.isValidElement(rawDetail)) return rawDetail; if (rawDetail.type !== React.Fragment) return rawDetail; - const children = React.Children.toArray(rawDetail.props.children); + const children = React.Children.toArray((rawDetail.props as { children?: React.ReactNode }).children); let mergedMarkdown: string | undefined; let mergedMetadata: React.ReactElement | undefined; let mergedIsLoading: boolean | undefined; diff --git a/src/renderer/src/raycast-api/oauth/oauth-service-core.ts b/src/renderer/src/raycast-api/oauth/oauth-service-core.ts index 0a923781..7fb945ae 100644 --- a/src/renderer/src/raycast-api/oauth/oauth-service-core.ts +++ b/src/renderer/src/raycast-api/oauth/oauth-service-core.ts @@ -25,6 +25,10 @@ export class OAuthServiceCore { return (override && override.trim()) || this.options.clientId; } + getPersonalAccessToken(): string | undefined { + return this.options.personalAccessToken; + } + setClientIdOverride(value: string): void { const key = oauthClientIdOverrideKey(this.getProviderKey()); const trimmed = (value || '').trim(); diff --git a/src/renderer/src/raycast-api/oauth/with-access-token.tsx b/src/renderer/src/raycast-api/oauth/with-access-token.tsx index 83b356d8..e2440719 100644 --- a/src/renderer/src/raycast-api/oauth/with-access-token.tsx +++ b/src/renderer/src/raycast-api/oauth/with-access-token.tsx @@ -16,10 +16,11 @@ export function withAccessToken(options: any) { const shouldInvokeOnAuthorize = !(options instanceof OAuthService); const authorizeForNoView = async (): Promise => { if (options instanceof OAuthService) { - if (options?.personalAccessToken) { - accessTokenValue = options.personalAccessToken; + const personalAccessToken = options.getPersonalAccessToken(); + if (personalAccessToken) { + accessTokenValue = personalAccessToken; accessTokenType = 'personal'; - await Promise.resolve(options.onAuthorize?.({ token: accessTokenValue, type: 'personal' })); + await Promise.resolve(options.onAuthorize?.({ token: personalAccessToken, type: 'personal' })); return; } diff --git a/src/renderer/src/settings/AITab.tsx b/src/renderer/src/settings/AITab.tsx index c27befdd..6813afd9 100644 --- a/src/renderer/src/settings/AITab.tsx +++ b/src/renderer/src/settings/AITab.tsx @@ -1224,7 +1224,7 @@ const AITab: React.FC = () => {

{t('settings.ai.llm.ollama.models')}

{ollamaRunning && (