Skip to content

Commit a196a4b

Browse files
ColinGPT9claude
andcommitted
Stop offering older versions, show real update progress, find old models
electron-updater's channel setter turns allowDowngrade on, so a feed older than the running build was offered as an update. Turn it back off after every channel change. A web-installer update downloads the small setup with progress, then writes the app package to its pending folder with none, which left the bar at 100% for the whole multi-gigabyte download. Watch that file's size on disk and report it as a second phase. Since d708b34 the bundled Ollama kept models under "Clips Kitty" while the data folder stayed "Clips Studio". Keep whichever folder already has models. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 9fd2f28 commit a196a4b

6 files changed

Lines changed: 129 additions & 15 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,19 @@ were often broken in a way that only showed up on somebody else's machine.
131131
to tell you that you had declined the permission request. It now says to open
132132
Google Auth Platform, then Audience, and press Publish app.
133133

134+
- **Updates no longer offer an older version.** An installed copy could offer to
135+
"update" itself to a version older than the one it was running whenever the
136+
update feed was behind it.
137+
138+
- **Downloading an update shows real progress.** After a small first step, the
139+
bar used to sit at 100% for the whole multi-gigabyte download, which looked
140+
frozen. From this version on it counts through the app files as they arrive.
141+
142+
- **AI models from older versions are found again.** Since 1.1.3 the app kept its
143+
models in a different folder from the rest of its data, so people coming from
144+
0.1.x downloaded their model a second time. It now keeps using whichever folder
145+
already has your models.
146+
134147
---
135148

136149
## 1.1.4: setup stops asking for a model you never chose

‎ui/src/main/index.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { app, BrowserWindow, Menu, Notification, dialog, ipcMain, shell } from 'electron'
22
import { execFileSync, spawn, type ChildProcess } from 'node:child_process'
3+
import { existsSync } from 'node:fs'
34
import { basename, join } from 'node:path'
45
import { setupUpdater } from './updater'
56
import { isMicrosoftStore } from './distribution'
@@ -39,7 +40,15 @@ function startOllama(): void {
3940
// in core/paths.py, or the engine and the runtime disagree about what is
4041
// downloaded.
4142
const localAppData = process.env.LOCALAPPDATA ?? join(app.getPath('home'), 'AppData', 'Local')
42-
const models = join(localAppData, 'Clips Kitty', 'data', 'models')
43+
// The 1.1.3 rename (d708b34) moved this folder to "Clips Kitty" while
44+
// core/paths.py rightly kept the data folder at "Clips Studio", so the two
45+
// stopped matching: models from earlier versions were stranded and fetched
46+
// again. Keep whichever folder already holds models, so 1.1.3 and 1.1.4
47+
// installs keep theirs; otherwise use the data folder's own name.
48+
const kittyModels = join(localAppData, 'Clips Kitty', 'data', 'models')
49+
const models = existsSync(join(kittyModels, 'manifests'))
50+
? kittyModels
51+
: join(localAppData, 'Clips Studio', 'data', 'models')
4352

4453
ollama = spawn(exe, ['serve'], {
4554
stdio: 'ignore',

‎ui/src/main/updater.ts‎

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
// publish block in electron-builder.yml.
1515

1616
import { app, ipcMain, type BrowserWindow } from 'electron'
17-
import { readFileSync, writeFileSync } from 'node:fs'
17+
import { readFileSync, statSync, writeFileSync } from 'node:fs'
1818
import { join } from 'node:path'
1919
import { autoUpdater, type UpdateInfo } from 'electron-updater'
2020
import { isMicrosoftStore } from './distribution'
@@ -62,6 +62,10 @@ function send(channel: string, payload: unknown): void {
6262
* `beta.yml`, `alpha.yml`) and the updater fetches exactly the one named. */
6363
function applyChannel(channel: Channel): void {
6464
autoUpdater.channel = channel === 'stable' ? 'latest' : channel
65+
// electron-updater's channel setter quietly turns downgrades on. Left that
66+
// way, any feed older than this build is offered as an "update": 1.2.0
67+
// offered 1.1.4 while its own feed was not yet published. Updates only go up.
68+
autoUpdater.allowDowngrade = false
6569
}
6670

6771
/** True while a pre-release check is in flight that a stable check will follow.
@@ -97,6 +101,58 @@ async function check(): Promise<{ ok: boolean; reason?: string }> {
97101
}
98102
}
99103

104+
/** The update on offer, and the size of the app files its web installer
105+
* fetches after itself (latest.yml's `packages`). */
106+
let offered: { version: string; packageSize: number } | null = null
107+
let packageTimer: ReturnType<typeof setInterval> | null = null
108+
let packageStarted = false
109+
110+
function packageSize(info: UpdateInfo): number {
111+
const packages = (info as UpdateInfo & { packages?: Record<string, { size?: number }> }).packages
112+
const first = packages ? Object.values(packages)[0] : undefined
113+
return typeof first?.size === 'number' ? first.size : 0
114+
}
115+
116+
/** Report the app files download as it happens.
117+
*
118+
* A web-installer update is two downloads. electron-updater reports progress
119+
* for the first, the ~1 MB setup, then writes the multi-gigabyte app package
120+
* straight to its pending folder without reporting anything, which left the
121+
* bar at "100% · 1 MB of 1 MB" for as long as that took and read as frozen.
122+
* The file is written in place, so its size on disk is the real figure. */
123+
function watchPackageDownload(): void {
124+
stopPackageWatch()
125+
packageStarted = false
126+
const target = offered
127+
if (!target) return
128+
packageTimer = setInterval(() => {
129+
const helper = (autoUpdater as unknown as {
130+
downloadedUpdateHelper?: { cacheDirForPendingUpdate: string }
131+
}).downloadedUpdateHelper
132+
if (!helper) return
133+
let transferred = 0
134+
try {
135+
transferred = statSync(join(helper.cacheDirForPendingUpdate, `package-${target.version}.7z`)).size
136+
} catch {
137+
return // the app files download has not started yet
138+
}
139+
packageStarted = true
140+
const total = target.packageSize
141+
send('update:state', {
142+
state: 'downloading',
143+
phase: 'package',
144+
transferred,
145+
total: total || undefined,
146+
percent: total ? Math.min(99, Math.round((transferred / total) * 100)) : undefined
147+
})
148+
}, 1000)
149+
}
150+
151+
function stopPackageWatch(): void {
152+
if (packageTimer) clearInterval(packageTimer)
153+
packageTimer = null
154+
}
155+
100156
export function setupUpdater(win: BrowserWindow): void {
101157
mainWindow = win
102158

@@ -136,6 +192,7 @@ export function setupUpdater(win: BrowserWindow): void {
136192
send('update:state', { state: 'none' })
137193
return
138194
}
195+
offered = { version: info.version, packageSize: packageSize(info) }
139196
send('update:state', {
140197
state: 'available',
141198
version: info.version,
@@ -146,21 +203,27 @@ export function setupUpdater(win: BrowserWindow): void {
146203

147204
autoUpdater.on('update-not-available', () => send('update:state', { state: 'none' }))
148205

149-
autoUpdater.on('download-progress', (p) =>
206+
autoUpdater.on('download-progress', (p) => {
207+
// Once the app files are downloading, the watcher reports them; this
208+
// event would otherwise overwrite that with the finished setup's 100%.
209+
if (packageStarted) return
150210
send('update:state', {
151211
state: 'downloading',
212+
phase: 'installer',
152213
percent: Math.round(p.percent),
153214
transferred: p.transferred,
154215
total: p.total,
155216
bytesPerSecond: p.bytesPerSecond
156217
})
157-
)
218+
})
158219

159-
autoUpdater.on('update-downloaded', (info: UpdateInfo) =>
220+
autoUpdater.on('update-downloaded', (info: UpdateInfo) => {
221+
stopPackageWatch()
160222
send('update:state', { state: 'ready', version: info.version })
161-
)
223+
})
162224

163225
autoUpdater.on('error', (err) => {
226+
stopPackageWatch()
164227
// An empty pre-release feed is about to be retried against stable, and
165228
// the retry reports the real outcome.
166229
if (willRetryOnStable) return
@@ -179,12 +242,15 @@ export function setupUpdater(win: BrowserWindow): void {
179242
})
180243

181244
ipcMain.handle('update:download', async () => {
245+
watchPackageDownload()
182246
try {
183247
await autoUpdater.downloadUpdate()
184248
return { ok: true }
185249
} catch (e) {
186250
send('update:state', { state: 'error', message: String(e) })
187251
return { ok: false }
252+
} finally {
253+
stopPackageWatch()
188254
}
189255
})
190256

‎ui/src/renderer/src/components/UpdateBanner.tsx‎

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { useEffect, useState } from 'react'
22

3-
function mb(n?: number): string {
4-
return n ? `${(n / 1e6).toFixed(0)} MB` : ''
3+
/** "813 MB" or "6.2 GB": update downloads span both. */
4+
function size(n?: number): string {
5+
if (!n) return ''
6+
return n >= 1e9 ? `${(n / 1e9).toFixed(1)} GB` : `${(n / 1e6).toFixed(0)} MB`
57
}
68

79
/** Strips HTML tags from release notes, which arrive as markup from the
@@ -57,19 +59,34 @@ export default function UpdateBanner(): JSX.Element | null {
5759
const bar = 'w-full px-4 py-2.5 flex items-center gap-3 text-sm border-b'
5860

5961
if (s.state === 'downloading') {
62+
// Two downloads: the small setup, then the app files. Between them the
63+
// updater is briefly silent, and a bar parked at 100% reads as frozen, so
64+
// that gap gets its own words instead of a finished-looking number.
65+
const packagePhase = s.phase === 'package'
6066
const pct = s.percent ?? 0
67+
const betweenDownloads = !packagePhase && pct >= 100
68+
const label = packagePhase
69+
? 'Downloading the app files…'
70+
: betweenDownloads
71+
? 'Preparing the app files download…'
72+
: 'Downloading update…'
73+
const detail = betweenDownloads
74+
? 'This can take a while'
75+
: packagePhase && !s.total
76+
? `${size(s.transferred)} downloaded`
77+
: `${pct}%${s.total ? ` · ${size(s.transferred)} of ${size(s.total)}` : ''}`
6178
return (
6279
<div className={`${bar} bg-surface border-raised/60`}>
63-
<span className="shrink-0 font-medium">Downloading update…</span>
80+
<span className="shrink-0 font-medium">{label}</span>
6481
<div className="flex-1 h-2 rounded-full bg-raised overflow-hidden max-w-md">
6582
<div
66-
className="h-full bg-accent rounded-full transition-[width] duration-500"
67-
style={{ width: `${Math.max(2, pct)}%` }}
83+
className={`h-full bg-accent rounded-full transition-[width] duration-500${
84+
betweenDownloads ? ' animate-pulse' : ''
85+
}`}
86+
style={{ width: `${betweenDownloads ? 100 : Math.max(2, pct)}%` }}
6887
/>
6988
</div>
70-
<span className="text-xs text-muted tabular-nums shrink-0">
71-
{pct}%{s.total ? ` · ${mb(s.transferred)} of ${mb(s.total)}` : ''}
72-
</span>
89+
<span className="text-xs text-muted tabular-nums shrink-0">{detail}</span>
7390
</div>
7491
)
7592
}

‎ui/src/renderer/src/env.d.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ interface UpdateState {
1616
version?: string
1717
notes?: string
1818
date?: string
19+
/** A web-installer update downloads twice: the small setup, then the app
20+
* files. Which of the two the numbers describe. */
21+
phase?: 'installer' | 'package'
1922
percent?: number
2023
transferred?: number
2124
total?: number

‎ui/src/renderer/src/pages/Settings.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,13 @@ function UpdateCard(): JSX.Element {
415415
case 'available':
416416
return `Version ${state.version} is available — see the bar at the top.`
417417
case 'downloading':
418-
return `Downloading… ${state.percent ?? 0}%`
418+
if (state.phase === 'package')
419+
return state.percent === undefined
420+
? 'Downloading the app files…'
421+
: `Downloading the app files… ${state.percent}%`
422+
return (state.percent ?? 0) >= 100
423+
? 'Preparing the app files download…'
424+
: `Downloading… ${state.percent ?? 0}%`
419425
case 'ready':
420426
return `Version ${state.version} is ready to install.`
421427
case 'none':

0 commit comments

Comments
 (0)