fix: desktop not-found panel gives app advice, not npm (#1701)

For a bundled CLI the not-found panel now tells desktop users to reopen or reinstall the app instead of installing from npm. Strings in all six locales.
This commit is contained in:
Resham Joshi 2026-10-09 12:31:19 +02:00 • committed by GitHub
parent 1bb697b7cf
commit a0e66e3fcd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 67 additions and 5 deletions

View file

@ -30,7 +30,7 @@ export const NO_UPDATE_STATUS: UpdateStatus = { currentVersion: '', latestVersio
// Result envelope: handlers never throw across IPC so the structured error
// `kind` survives contextBridge serialization. preload.ts unwraps it.
export type Envelope<T = unknown> = { ok: true; value: T } | { ok: false; error: { kind: string; message: string; cold?: true } }
export type Envelope<T = unknown> = { ok: true; value: T } | { ok: false; error: { kind: string; message: string; cold?: true; stage?: string } }
// The first overview fetch after boot hydrates a cold cache from scratch (a full
// history parse). That can far exceed the 45s read timeout, and killing it means
@ -388,8 +388,8 @@ function priceOverrideArgs(model: string, rates: PriceRates | undefined): string
]
}
function toEnvelopeError(err: unknown): { kind: string; message: string } {
if (err instanceof CliError) return { kind: err.kind, message: sanitizeError(err.message) }
function toEnvelopeError(err: unknown): { kind: string; message: string; stage?: string } {
if (err instanceof CliError) return { kind: err.kind, message: sanitizeError(err.message), ...(err.kind === 'not-found' && err.detail ? { stage: err.detail } : {}) }
return { kind: 'nonzero', message: sanitizeError(err instanceof Error ? err.message : String(err)) }
}
@ -526,7 +526,7 @@ export function createBridgeHandlers(deps: Deps): Record<string, Handler> {
const bootedAt = Date.now()
const stillCold = (): boolean =>
!overviewWarmed && Date.now() - (coldStartBegan ?? bootedAt) < WARMUP_TIMEOUT_MS
const coldError = (err: unknown): { kind: string; message: string; cold?: true } => {
const coldError = (err: unknown): { kind: string; message: string; cold?: true; stage?: string } => {
const error = toEnvelopeError(err)
return stillCold() && error.kind === 'timeout' ? { ...error, cold: true } : error
}

View file

@ -0,0 +1,24 @@
// @vitest-environment jsdom
import { render } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { CliErrorPanel } from './CliErrorPanel'
describe('CliErrorPanel not-found', () => {
it('gives app advice, not npm, when the bundled engine is unreachable', () => {
const { container } = render(<CliErrorPanel error={{ kind: 'not-found', message: 'x', stage: 'bundled-missing' }} />)
expect(container.textContent).toContain('reinstall the app from codeburn.app')
expect(container.textContent).not.toContain('npm')
})
it('tells the user to allow CodeBurn when access was denied', () => {
const { container } = render(<CliErrorPanel error={{ kind: 'not-found', message: 'x', stage: 'bundled-denied' }} />)
expect(container.textContent).toContain('Security software')
expect(container.textContent).not.toContain('npm')
})
it('keeps the npm advice when no bundled CLI is configured', () => {
const { container } = render(<CliErrorPanel error={{ kind: 'not-found', message: 'x', stage: 'no-path-match' }} />)
expect(container.textContent).toContain('npm i -g codeburn')
})
})

View file

@ -12,6 +12,10 @@ export function isColdHydrating(error: CliError | null): boolean {
return error?.cold === true
}
function isBundledNotFound(error: CliError): boolean {
return error.kind === 'not-found' && error.stage?.startsWith('bundled-') === true
}
export function cliErrorDisplay(error: CliError): { title: string; message: string; tone: 'amber' | 'red' | 'muted' } {
if (isColdHydrating(error)) {
return {
@ -20,6 +24,13 @@ export function cliErrorDisplay(error: CliError): { title: string; message: stri
tone: 'muted',
}
}
if (isBundledNotFound(error)) {
return {
title: t('shell.error.notFound.bundledTitle'),
message: t(error.stage === 'bundled-denied' ? 'shell.error.notFound.bundledDenied' : 'shell.error.notFound.bundledMessage'),
tone: 'muted',
}
}
if (error.kind === 'not-found') {
return {
title: t('shell.error.notFound.title'),
@ -50,6 +61,13 @@ export function CliErrorText({ error }: { error: CliError }) {
export function CliErrorPanel({ error, subject = 'usage' }: { error: CliError; subject?: string }) {
const display = cliErrorDisplay(error)
if (isBundledNotFound(error)) {
return (
<Panel title={display.title}>
<p style={{ color: 'var(--mut)', margin: 0, fontSize: 'var(--fs-body)' }}>{display.message}</p>
</Panel>
)
}
if (error.kind === 'not-found') {
return (
<Panel title={display.title}>

View file

@ -87,6 +87,9 @@ export const shell: SectionCatalog = {
'shell.error.notFound.pathSuffix': " command, but it isn't on your PATH yet.",
'shell.error.notFound.installPrefix': 'Install it with ',
'shell.error.notFound.installSuffix': ', then reopen this window.',
'shell.error.notFound.bundledTitle': "CodeBurn can't reach its engine",
'shell.error.notFound.bundledMessage': 'Quit and reopen CodeBurn. If it keeps happening, reinstall the app from codeburn.app.',
'shell.error.notFound.bundledDenied': 'Security software may be blocking CodeBurn. Allow it, then quit and reopen CodeBurn. If it keeps happening, reinstall the app from codeburn.app.',
'shell.error.permission.title': 'Permission denied',
'shell.error.permission.message': 'permission denied; grant Full Disk Access',
'shell.error.generic.title': "Couldn't read data",
@ -184,6 +187,9 @@ export const shell: SectionCatalog = {
'shell.error.notFound.pathSuffix': ", mais elle n'est pas encore dans votre PATH.",
'shell.error.notFound.installPrefix': 'Installez-la avec ',
'shell.error.notFound.installSuffix': ', puis rouvrez cette fenêtre.',
'shell.error.notFound.bundledTitle': 'CodeBurn ne trouve pas son moteur',
'shell.error.notFound.bundledMessage': "Quittez et rouvrez CodeBurn. Si le problème persiste, réinstallez l'app depuis codeburn.app.",
'shell.error.notFound.bundledDenied': "Un logiciel de sécurité bloque peut-être CodeBurn. Autorisez-le, puis quittez et rouvrez CodeBurn. Si le problème persiste, réinstallez l'app depuis codeburn.app.",
'shell.error.permission.title': 'Autorisation refusée',
'shell.error.permission.message': "autorisation refusée ; accordez l'accès complet au disque",
'shell.error.generic.title': 'Impossible de lire les données',
@ -281,6 +287,9 @@ export const shell: SectionCatalog = {
'shell.error.notFound.pathSuffix': ' コマンドを実行しますが、まだ PATH に含まれていません。',
'shell.error.notFound.installPrefix': '',
'shell.error.notFound.installSuffix': ' でインストールし、このウィンドウを開き直してください。',
'shell.error.notFound.bundledTitle': 'CodeBurn がエンジンに接続できません',
'shell.error.notFound.bundledMessage': 'CodeBurn を終了して開き直してください。改善しない場合は codeburn.app からアプリを再インストールしてください。',
'shell.error.notFound.bundledDenied': 'セキュリティソフトが CodeBurn をブロックしている可能性があります。許可したうえで、CodeBurn を終了して開き直してください。改善しない場合は codeburn.app からアプリを再インストールしてください。',
'shell.error.permission.title': 'アクセスが拒否されました',
'shell.error.permission.message': 'アクセスが拒否されました。フルディスクアクセスを許可してください',
'shell.error.generic.title': 'データを読み込めませんでした',
@ -378,6 +387,9 @@ export const shell: SectionCatalog = {
'shell.error.notFound.pathSuffix': ' 명령을 실행하지만, 아직 PATH에 없습니다.',
'shell.error.notFound.installPrefix': '',
'shell.error.notFound.installSuffix': '으로 설치한 다음 이 창을 다시 여세요.',
'shell.error.notFound.bundledTitle': 'CodeBurn이 엔진에 연결할 수 없습니다',
'shell.error.notFound.bundledMessage': 'CodeBurn을 종료한 다음 다시 여세요. 계속되면 codeburn.app에서 앱을 다시 설치하세요.',
'shell.error.notFound.bundledDenied': '보안 소프트웨어가 CodeBurn을 차단하고 있을 수 있습니다. 허용한 다음 CodeBurn을 종료하고 다시 여세요. 계속되면 codeburn.app에서 앱을 다시 설치하세요.',
'shell.error.permission.title': '권한이 거부됨',
'shell.error.permission.message': '권한이 거부됨. 전체 디스크 접근 권한을 허용하세요',
'shell.error.generic.title': '데이터를 읽을 수 없습니다',
@ -475,6 +487,9 @@ export const shell: SectionCatalog = {
'shell.error.notFound.pathSuffix': ' 命令,但它还不在您的 PATH 中。',
'shell.error.notFound.installPrefix': '使用 ',
'shell.error.notFound.installSuffix': ' 安装,然后重新打开此窗口。',
'shell.error.notFound.bundledTitle': 'CodeBurn 无法连接其引擎',
'shell.error.notFound.bundledMessage': '请退出并重新打开 CodeBurn。如果问题持续,请从 codeburn.app 重新安装应用。',
'shell.error.notFound.bundledDenied': '安全软件可能拦截了 CodeBurn。请允许它运行,然后退出并重新打开 CodeBurn。如果问题持续,请从 codeburn.app 重新安装应用。',
'shell.error.permission.title': '权限被拒绝',
'shell.error.permission.message': '权限被拒绝;请授予完全磁盘访问权限',
'shell.error.generic.title': '无法读取数据',
@ -572,6 +587,9 @@ export const shell: SectionCatalog = {
'shell.error.notFound.pathSuffix': ' 命令,但它尚未加入您的 PATH。',
'shell.error.notFound.installPrefix': '使用 ',
'shell.error.notFound.installSuffix': ' 安裝,然後重新開啟此視窗。',
'shell.error.notFound.bundledTitle': 'CodeBurn 無法連線至其引擎',
'shell.error.notFound.bundledMessage': '請結束並重新開啟 CodeBurn。如果問題持續,請從 codeburn.app 重新安裝應用程式。',
'shell.error.notFound.bundledDenied': '安全軟體可能封鎖了 CodeBurn。請允許它執行,然後結束並重新開啟 CodeBurn。如果問題持續,請從 codeburn.app 重新安裝應用程式。',
'shell.error.permission.title': '權限遭拒',
'shell.error.permission.message': '權限遭拒;請授予完整磁碟取用權限',
'shell.error.generic.title': '無法讀取資料',

View file

@ -16,7 +16,7 @@ export const codeburn: CodeburnBridge = window.codeburn
export function normalizeCliError(err: unknown): CliError {
if (err && typeof err === 'object' && 'kind' in err && typeof (err as CliError).kind === 'string') {
const e = err as CliError
return { kind: e.kind, message: e.message ?? 'codeburn CLI error', ...(e.cold ? { cold: true as const } : {}) }
return { kind: e.kind, message: e.message ?? 'codeburn CLI error', ...(e.cold ? { cold: true as const } : {}), ...(e.stage ? { stage: e.stage } : {}) }
}
const message = err instanceof Error ? err.message : String(err)
return { kind: 'nonzero', message }

View file

@ -23,6 +23,8 @@ export interface CliError {
* broken install, so the UI keeps its splash/progress state. Optional so an
* older preload simply never sets it. */
cold?: true
/** For `not-found` only: the resolution stage enum from the main process. */
stage?: string
}
export type AliasRow = { from: string; to: string }