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
5 changes: 5 additions & 0 deletions .changeset/pin-subagent-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pymodel/pythinker-code": patch
---

Add a setting to pin every subagent to the selected model, use the dark banner in every sidebar, and show Pythinker desktop updates as one continuous download.
4 changes: 3 additions & 1 deletion apps/desktop/src/updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ function hasUpdateConfig(): boolean {
function configureExplicitConsent(): void {
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = false
autoUpdater.disableDifferentialDownload = true
autoUpdater.channel = settings.channel === 'stable' ? STABLE_UPDATER_CHANNEL : settings.channel
autoUpdater.allowPrerelease = settings.channel !== 'stable'
autoUpdater.allowDowngrade = false
Expand Down Expand Up @@ -346,6 +347,7 @@ function wireUpdaterEvents(): void {
})
})
autoUpdater.on('download-progress', (progress: ProgressInfo) => {
if (state.status !== 'downloading') return
updateState({
status: 'downloading',
percent: progress.percent,
Expand Down Expand Up @@ -444,7 +446,7 @@ export function setAutoUpdate(enabled: boolean): UpdateState {
}
if (enabled) {
scheduleChecks()
if (!wasEnabled) void checkForUpdatesNow()
if (!wasEnabled && state.availableVersion === undefined) void checkForUpdatesNow()
} else {
clearTimers()
}
Expand Down
86 changes: 86 additions & 0 deletions apps/desktop/tests/updater.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ vi.mock('electron-updater', () => ({
checkForUpdates: vi.fn(),
downloadUpdate: vi.fn(() => Promise.resolve([])),
quitAndInstall: vi.fn(),
disableDifferentialDownload: false,
},
},
}))
Expand Down Expand Up @@ -69,6 +70,7 @@ afterEach(() => {
autoUpdater.autoInstallOnAppQuit = undefined as unknown as boolean
autoUpdater.allowPrerelease = undefined as unknown as boolean
autoUpdater.allowDowngrade = undefined as unknown as boolean
autoUpdater.disableDifferentialDownload = false
;(autoUpdater as unknown as { _channel: string | null })._channel = null
})

Expand Down Expand Up @@ -241,6 +243,7 @@ describe('strict update consent', () => {
expect(localAutoUpdater.allowDowngrade).toBe(false)
expect(localAutoUpdater.autoDownload).toBe(false)
expect(localAutoUpdater.autoInstallOnAppQuit).toBe(false)
expect(localAutoUpdater.disableDifferentialDownload).toBe(true)
expect(localAutoUpdater.checkForUpdates).not.toHaveBeenCalled()
expect(localAutoUpdater.downloadUpdate).not.toHaveBeenCalled()
expect(localAutoUpdater.quitAndInstall).not.toHaveBeenCalled()
Expand Down Expand Up @@ -339,6 +342,7 @@ describe('strict update consent', () => {
const {
getUpdateState: getLocalUpdateState,
initUpdater: initLocalUpdater,
startUpdateDownload: startLocalUpdateDownload,
} = await import('../src/updater')
vi.mocked(localApp.getPath).mockReturnValue(directory)
Object.defineProperty(localApp, 'isPackaged', { configurable: true, value: true })
Expand Down Expand Up @@ -366,6 +370,7 @@ describe('strict update consent', () => {
releaseDate: '2026-08-22T12:00:00.000Z',
releaseNotes: [{ note: 'First change' }, { note: null }, { note: 'Second change' }],
})
startLocalUpdateDownload()
progress?.({ percent: 42.5, transferred: 425, total: 1_000, bytesPerSecond: 85 })

expect(getLocalUpdateState()).toMatchObject({
Expand All @@ -381,6 +386,38 @@ describe('strict update consent', () => {
})
})

it('keeps an available update downloadable when automatic checks are enabled', async () => {
vi.resetModules()
const directory = temporaryDirectory()
writeFileSync(join(directory, 'app-update.yml'), '', 'utf8')
writeFileSync(join(directory, 'update-settings.json'), '{"autoUpdate":false}\n', 'utf8')
const { app: localApp } = await import('electron')
const { default: localElectronUpdater } = await import('electron-updater')
const {
getUpdateState: getLocalUpdateState,
initUpdater: initLocalUpdater,
setAutoUpdate: setLocalAutoUpdate,
startUpdateDownload: startLocalUpdateDownload,
} = await import('../src/updater')
const localAutoUpdater = localElectronUpdater.autoUpdater
vi.mocked(localApp.getPath).mockReturnValue(directory)
Object.defineProperty(localApp, 'isPackaged', { configurable: true, value: true })
Object.defineProperty(process, 'resourcesPath', { configurable: true, value: directory })

initLocalUpdater(() => undefined)
const available = vi.mocked(localAutoUpdater.on).mock.calls.find(
([event]) => event === 'update-available',
)?.[1] as ((info: { version: string }) => void) | undefined
available?.({ version: '1.2.3' })

expect(setLocalAutoUpdate(true)).toMatchObject({ status: 'available', availableVersion: '1.2.3' })
expect(localAutoUpdater.checkForUpdates).not.toHaveBeenCalled()
expect(startLocalUpdateDownload()).toMatchObject({ status: 'downloading' })
expect(localAutoUpdater.downloadUpdate).toHaveBeenCalledOnce()
setLocalAutoUpdate(false)
expect(getLocalUpdateState()).toMatchObject({ status: 'downloading' })
})

it('downloads and installs only through separate explicit actions', async () => {
vi.resetModules()
const directory = temporaryDirectory()
Expand Down Expand Up @@ -420,6 +457,55 @@ describe('strict update consent', () => {
expect(readUpdateSettings(directory)).toMatchObject({ pendingInstallVersion: '1.2.3' })
})

it('does not return to downloading after the update is ready', async () => {
vi.resetModules()
const directory = temporaryDirectory()
writeFileSync(join(directory, 'app-update.yml'), '', 'utf8')
writeFileSync(join(directory, 'update-settings.json'), '{"autoUpdate":false}\n', 'utf8')
const { app: localApp } = await import('electron')
const { default: localElectronUpdater } = await import('electron-updater')
const {
getUpdateState: getLocalUpdateState,
initUpdater: initLocalUpdater,
startUpdateDownload: startLocalUpdateDownload,
} = await import('../src/updater')
const localAutoUpdater = localElectronUpdater.autoUpdater
vi.mocked(localApp.getPath).mockReturnValue(directory)
Object.defineProperty(localApp, 'isPackaged', { configurable: true, value: true })
Object.defineProperty(process, 'resourcesPath', { configurable: true, value: directory })

initLocalUpdater(() => undefined)
const available = vi.mocked(localAutoUpdater.on).mock.calls.find(
([event]) => event === 'update-available',
)?.[1] as ((info: { version: string }) => void) | undefined
const progress = vi.mocked(localAutoUpdater.on).mock.calls.find(
([event]) => event === 'download-progress',
)?.[1] as ((info: {
percent: number
transferred: number
total: number
bytesPerSecond: number
}) => void) | undefined
const downloaded = vi.mocked(localAutoUpdater.on).mock.calls.find(
([event]) => event === 'update-downloaded',
)?.[1] as ((info: { version: string }) => void) | undefined

available?.({ version: '1.2.3' })
startLocalUpdateDownload()
progress?.({ percent: 100, transferred: 1_000, total: 1_000, bytesPerSecond: 80 })
downloaded?.({ version: '1.2.3' })
progress?.({ percent: 1, transferred: 10, total: 1_000, bytesPerSecond: 20 })

expect(getLocalUpdateState()).toMatchObject({
status: 'downloaded',
availableVersion: '1.2.3',
percent: 100,
transferred: 1_000,
total: 1_000,
})
expect(localAutoUpdater.downloadUpdate).toHaveBeenCalledOnce()
})

it('does not let a scheduled check overwrite a downloaded update', async () => {
vi.useFakeTimers()
try {
Expand Down
4 changes: 2 additions & 2 deletions apps/pythinker-code/dist-web/.web-bundle-manifest.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"sourceHash": "965c37d0a139928c9e6107941d28eb228a6be4d212d1bbd110338a83ef8646a2",
"sourceFileCount": 404
"sourceHash": "f88e4160eddd99fef98d31820eb0a5e660fc8209a97edb99e62b445d7f9b7365",
"sourceFileCount": 403
}
Loading
Loading