diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index b5e8cfd4..36d4a79c 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -12,6 +12,9 @@ on: jobs: claude-review: + # Fork PRs do not receive the OIDC token/secrets required by + # anthropics/claude-code-action on pull_request workflows. + if: github.event.pull_request.head.repo.full_name == github.repository # Optional: Filter by PR author # if: | # github.event.pull_request.user.login == 'external-contributor' || @@ -41,4 +44,3 @@ jobs: prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md # or https://code.claude.com/docs/en/cli-reference for available options - diff --git a/.github/workflows/project-checks.yml b/.github/workflows/project-checks.yml new file mode 100644 index 00000000..536dce79 --- /dev/null +++ b/.github/workflows/project-checks.yml @@ -0,0 +1,58 @@ +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: Typecheck renderer + run: npm run typecheck:renderer + + - name: Build renderer + run: npm run build:renderer 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 f262a8c1..75b50ded 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,13 @@ "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'", "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" @@ -42,12 +43,9 @@ }, "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", - "electron-liquid-glass": "^1.1.1", "electron-updater": "^6.7.3", "esbuild": "^0.19.12", "lucide-react": "^0.312.0", @@ -57,6 +55,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/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); diff --git a/scripts/check-i18n.mjs b/scripts/check-i18n.mjs index d6835fe5..780ac61c 100644 --- a/scripts/check-i18n.mjs +++ b/scripts/check-i18n.mjs @@ -3,7 +3,11 @@ import path from 'path'; const localesDir = path.resolve('src/renderer/src/i18n/locales'); const baseLocale = 'en'; -const strictLocales = new Set((process.env.SUPERCMD_STRICT_LOCALES || 'ko').split(',').map((item) => item.trim()).filter(Boolean)); +const localeFiles = fs.readdirSync(localesDir).filter((file) => file.endsWith('.json') && file !== `${baseLocale}.json`).sort(); +const defaultStrictLocales = localeFiles.map((file) => file.replace(/\.json$/, '')); +const strictLocales = new Set( + (process.env.SUPERCMD_STRICT_LOCALES || defaultStrictLocales.join(',')).split(',').map((item) => item.trim()).filter(Boolean), +); function isObject(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); @@ -41,7 +45,6 @@ function walk(baseNode, localeNode, currentPath, result) { } const baseMessages = JSON.parse(fs.readFileSync(path.join(localesDir, `${baseLocale}.json`), 'utf8')); -const localeFiles = fs.readdirSync(localesDir).filter((file) => file.endsWith('.json') && file !== `${baseLocale}.json`); let hasStrictFailure = false; 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.'); 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`); 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/locales/it.json b/src/renderer/src/i18n/locales/it.json index 75ae34be..82538b88 100644 --- a/src/renderer/src/i18n/locales/it.json +++ b/src/renderer/src/i18n/locales/it.json @@ -375,6 +375,12 @@ "elevenlabsWarning": "STT di ElevenLabs selezionato. {action} la chiave API di ElevenLabs in Chiavi API.", "elevenlabsReady": "STT di ElevenLabs selezionato. La trascrizione cloud userà la tua chiave ElevenLabs.", "recognitionLanguage": "Lingua di riconoscimento", + "vocabulary": { + "label": "Vocabolario personalizzato", + "placeholder": "Kotlin, Electron, Raycast, SuperCmd…", + "hint": "Facoltativo. Elenca parole poco comuni, nomi o termini tecnici (separati da virgole o da nuove righe) per orientare il riconoscimento verso l'ortografia corretta.", + "tokenWarning": "Circa {count} token. Whisper usa all'incirca solo i primi 224: le parole oltre quel limite vengono ignorate." + }, "providerInfo": { "parakeet": "Trascrizione offline sul dispositivo con Parakeet TDT v3. Viene eseguita su Apple Neural Engine. Richiede un riscaldamento del modello al primo utilizzo. Scarica il modello qui sotto prima di usare la dettatura.", "qwen3": "Trascrizione offline sul dispositivo con Qwen3 ASR. Supporta oltre 30 lingue, richiede macOS 15+ e si riscalda al primo utilizzo.", @@ -611,6 +617,18 @@ "openedExisting": "Cartella degli script personalizzati aperta.", "failed": "Impossibile aprire la cartella degli script personalizzati." }, + "appSearchScope": { + "title": "Ambito di ricerca delle applicazioni", + "description": "Cartelle in cui SuperCmd cerca le applicazioni installate da mostrare nel launcher.", + "add": "Aggiungi cartella", + "remove": "Rimuovi", + "empty": "Nessuna cartella aggiunta. Verranno usate le cartelle applicazioni di sistema predefinite.", + "added": "Cartella aggiunta all'ambito di ricerca.", + "removed": "Cartella rimossa dall'ambito di ricerca.", + "duplicate": "Cartella già presente nell'elenco.", + "failed": "Impossibile aggiornare l'ambito di ricerca.", + "restartHint": "Riavvia SuperCmd affinché le modifiche alle cartelle abbiano effetto." + }, "emojiPicker": { "enabled": "Abilita il selettore emoji", "enabledDesc": "Mostra suggerimenti emoji quando si digita un prefisso trigger.", 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 && (