Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const bridge = {
return () => { ipcRenderer.removeListener('codeburn:update', listener) }
},
platform: process.platform,
arch: process.arch,
}

contextBridge.exposeInMainWorld('codeburn', bridge)
4 changes: 2 additions & 2 deletions app/renderer/components/AboutModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useEffect, useState, type MouseEvent, type ReactNode } from 'react'
import { version } from '../../package.json'
import { FlameMark } from './FlameMark'
import { BUILD_STAMP } from '../lib/build'
import { releasePageUrl, useUpdateStatus } from '../hooks/useUpdateStatus'
import { updateDownloadUrl, useUpdateStatus } from '../hooks/useUpdateStatus'
import { codeburn } from '../lib/ipc'

export type SocialLink = {
Expand Down Expand Up @@ -81,7 +81,7 @@ export function AboutModal({ socials, onClose }: { socials: SocialLink[]; onClos
<button
type="button"
className="set-text-button"
onClick={() => { void codeburn.openExternal(releasePageUrl(status.tag!)) }}
onClick={() => { void codeburn.openExternal(updateDownloadUrl(status.tag!)) }}
>
Download
</button>
Expand Down
4 changes: 2 additions & 2 deletions app/renderer/components/UpdateBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from 'react'

import { releasePageUrl, useUpdateStatus } from '../hooks/useUpdateStatus'
import { updateDownloadUrl, useUpdateStatus } from '../hooks/useUpdateStatus'
import { codeburn } from '../lib/ipc'

const DISMISS_KEY = 'codeburn.updateDismissed'
Expand Down Expand Up @@ -32,7 +32,7 @@ export function UpdateBanner() {
<div role="status" className="update-banner">
<span>
Update available: CodeBurn {status.latestVersion} ·{' '}
<button type="button" className="set-text-button" onClick={() => { void codeburn.openExternal(releasePageUrl(tag)) }}>Download</button>
<button type="button" className="set-text-button" onClick={() => { void codeburn.openExternal(updateDownloadUrl(tag)) }}>Download</button>
</span>
<button type="button" className="set-text-button" onClick={dismiss}>Dismiss</button>
</div>
Expand Down
44 changes: 44 additions & 0 deletions app/renderer/hooks/useUpdateStatus.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, it, vi } from 'vitest'

vi.mock('../lib/ipc', () => ({
codeburn: { platform: 'linux', arch: 'x64' },
}))

import { directDownloadUrl, releasePageUrl, updateDownloadUrl } from './useUpdateStatus'

const TAG = 'desktop-v0.9.19'
const BASE = 'https://github.com/getagentseal/codeburn/releases/download/desktop-v0.9.19'

describe('directDownloadUrl', () => {
it('maps macOS arm64 to the arm64 dmg', () => {
expect(directDownloadUrl(TAG, 'darwin', 'arm64')).toBe(`${BASE}/CodeBurn-0.9.19-arm64.dmg`)
})

it('maps macOS x64 to the plain dmg', () => {
expect(directDownloadUrl(TAG, 'darwin', 'x64')).toBe(`${BASE}/CodeBurn-0.9.19.dmg`)
})

it('maps Windows to the Setup exe regardless of arch', () => {
expect(directDownloadUrl(TAG, 'win32', 'x64')).toBe(`${BASE}/CodeBurn-Setup-0.9.19.exe`)
expect(directDownloadUrl(TAG, 'win32', undefined)).toBe(`${BASE}/CodeBurn-Setup-0.9.19.exe`)
})

it('returns null for Linux (three formats, the user picks on the page)', () => {
expect(directDownloadUrl(TAG, 'linux', 'x64')).toBeNull()
})

it('returns null for unknown platforms, missing mac arch, and foreign tags', () => {
expect(directDownloadUrl(TAG, 'freebsd', 'x64')).toBeNull()
expect(directDownloadUrl(TAG, 'darwin', undefined)).toBeNull()
expect(directDownloadUrl('mac-v0.9.19', 'darwin', 'arm64')).toBeNull()
})
})

describe('updateDownloadUrl fallback', () => {
it('falls back to the release page when no direct asset fits', () => {
// The mocked bridge reports linux, where no single asset fits, so the
// click target is the release page.
expect(releasePageUrl(TAG)).toBe('https://github.com/getagentseal/codeburn/releases/tag/desktop-v0.9.19')
expect(updateDownloadUrl(TAG)).toBe(releasePageUrl(TAG))
})
})
31 changes: 29 additions & 2 deletions app/renderer/hooks/useUpdateStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,35 @@ export function useUpdateStatus(): UpdateStatus | null {
return status
}

/** GitHub release page for a desktop tag — the Download target (no site #get
* anchor exists). https-only, so it passes the openExternal allowlist. */
/** GitHub release page for a desktop tag — the fallback Download target when
* no single direct asset fits (Linux ships three formats) or the platform is
* unknown. https-only, so it passes the openExternal allowlist. */
export function releasePageUrl(tag: string): string {
return `https://github.com/getagentseal/codeburn/releases/tag/${tag}`
}

/**
* Direct asset download for the running platform, so Download saves the file
* instead of landing on a GitHub page. Filenames mirror the electron-builder
* output (app/DISTRIBUTION.md) and are version-derived from the tag. Returns
* null (callers fall back to the release page) for Linux — three formats, the
* user picks — and for unknown platforms or a preload without `arch`.
*/
export function directDownloadUrl(tag: string, platform: string | undefined, arch: string | undefined): string | null {
const version = tag.startsWith('desktop-v') ? tag.slice('desktop-v'.length) : null
if (!version) return null
let file: string | null = null
if (platform === 'darwin') {
if (!arch) return null
file = arch === 'arm64' ? `CodeBurn-${version}-arm64.dmg` : `CodeBurn-${version}.dmg`
} else if (platform === 'win32') {
file = `CodeBurn-Setup-${version}.exe`
}
if (!file) return null
return `https://github.com/getagentseal/codeburn/releases/download/${tag}/${file}`
}

/** The Download click target: direct asset when determinable, else the page. */
export function updateDownloadUrl(tag: string): string {
return directDownloadUrl(tag, codeburn.platform, codeburn.arch) ?? releasePageUrl(tag)
}
3 changes: 3 additions & 0 deletions app/renderer/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,9 @@ export interface CodeburnBridge {
getPlans(period: Period): Promise<StatusJson>
getActReport(): Promise<ActReportJson>
readonly platform: string
/** Node process.arch of the host ('arm64', 'x64', ...). Absent on preloads
* that predate the direct-download update link. */
readonly arch?: string
getModels(period: Period, provider: string, byTask: boolean, range?: DateRange): Promise<ModelReportRow[]>
getSessions(period: Period, provider: string, range?: DateRange): Promise<SessionRow[]>
getCompareModels(period: Period, provider: string): Promise<ModelStats[]>
Expand Down
4 changes: 2 additions & 2 deletions app/renderer/sections/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Panel } from '../components/Panel'
import { ProviderLogo } from '../components/ProviderLogo'
import type { Section } from '../components/Sidebar'
import { usePolled } from '../hooks/usePolled'
import { releasePageUrl, useUpdateStatus } from '../hooks/useUpdateStatus'
import { updateDownloadUrl, useUpdateStatus } from '../hooks/useUpdateStatus'
import { version as appVersion } from '../../package.json'
import { readDailyBudget } from '../lib/budget'
import { formatConverted, formatUsd } from '../lib/format'
Expand Down Expand Up @@ -208,7 +208,7 @@ function GeneralPane({ period, refreshToken, claudeConfigs, claudeConfigSource,
</div>
<div className="about-sec set-last-sec">
<div className="about-sec-h">About</div>
<div className="about-row"><span className="tx">Version {version}{updateNote && <small>{updateNote}</small>}</span><span className="r">{update?.updateAvailable && update.tag ? <button className="set-text-button" onClick={() => { void codeburn.openExternal(releasePageUrl(update.tag!)) }}>Download</button> : null}</span></div>
<div className="about-row"><span className="tx">Version {version}{updateNote && <small>{updateNote}</small>}</span><span className="r">{update?.updateAvailable && update.tag ? <button className="set-text-button" onClick={() => { void codeburn.openExternal(updateDownloadUrl(update.tag!)) }}>Download</button> : null}</span></div>
</div>
</div>
</section>
Expand Down