Skip to content

Commit 84dc920

Browse files
authored
feat: auto-download updates, floating restart banner, dismiss cooldown (#109)
* feat: auto-download updates, floating restart banner, dismiss cooldown - Auto-download updates in background after detection instead of requiring user to click a download button - Persist staged update path to storage so it survives app restarts without re-downloading - Restore staged update on startup if .app still exists on disk - Redesign update banner as a floating card matching the worktree modal design language - Only show banner when update is Ready (downloaded and extracted) - Dismiss cooldown: if user closes the banner, hide it for 1 day - Clean up staged update storage keys on quit-and-install * chore: bump version to 0.2.14
1 parent a7e358f commit 84dc920

4 files changed

Lines changed: 171 additions & 107 deletions

File tree

apps/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "code-oss-dev",
3-
"version": "0.2.13",
3+
"version": "0.2.14",
44
"distro": "d48ce3e61fa56a702d13bb450ceb3532620dbd46",
55
"author": {
66
"name": "Workstream Labs"

apps/desktop/src/vs/platform/update/electron-main/workstreamsUpdateService.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { IProductService } from '../../product/common/productService.js';
1717
import { asJson, IRequestService } from '../../request/common/request.js';
1818
import { IApplicationStorageMainService } from '../../storage/electron-main/storageMainService.js';
1919
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
20+
import { StorageScope, StorageTarget } from '../../storage/common/storage.js';
2021
import { AvailableForDownload, IUpdate, State, StateType, UpdateType } from '../common/update.js';
2122
import { IMeteredConnectionService } from '../../meteredConnection/common/meteredConnection.js';
2223
import { AbstractUpdateService, IUpdateURLOptions } from './abstractUpdateService.js';
@@ -32,6 +33,11 @@ import { Promises } from '../../../base/node/pfs.js';
3233
* 3. Extracts the .app bundle → Ready
3334
* 4. On restart: spawns a helper script that swaps the .app and relaunches
3435
*/
36+
const STAGED_UPDATE_PATH_KEY = 'workstreamsUpdate/stagedPath';
37+
const STAGED_UPDATE_COMMIT_KEY = 'workstreamsUpdate/stagedCommit';
38+
const STAGED_UPDATE_VERSION_KEY = 'workstreamsUpdate/stagedProductVersion';
39+
const STAGED_UPDATE_CHANGELOG_KEY = 'workstreamsUpdate/stagedChangelogUrl';
40+
3541
export class WorkstreamsUpdateService extends AbstractUpdateService implements IRelaunchHandler {
3642

3743
private stagedUpdatePath: string | undefined;
@@ -66,6 +72,50 @@ export class WorkstreamsUpdateService extends AbstractUpdateService implements I
6672
return true;
6773
}
6874

75+
protected override async postInitialize(): Promise<void> {
76+
const stagedPath = this.applicationStorageMainService.get(STAGED_UPDATE_PATH_KEY, StorageScope.APPLICATION);
77+
const stagedCommit = this.applicationStorageMainService.get(STAGED_UPDATE_COMMIT_KEY, StorageScope.APPLICATION);
78+
const stagedVersion = this.applicationStorageMainService.get(STAGED_UPDATE_VERSION_KEY, StorageScope.APPLICATION);
79+
const stagedChangelog = this.applicationStorageMainService.get(STAGED_UPDATE_CHANGELOG_KEY, StorageScope.APPLICATION);
80+
81+
if (stagedPath && stagedCommit && stagedCommit !== this.productService.commit) {
82+
try {
83+
const stat = await fs.promises.stat(stagedPath);
84+
if (stat.isDirectory()) {
85+
this.logService.info('workstreams-update#postInitialize - restoring staged update', { stagedPath, stagedCommit });
86+
this.stagedUpdatePath = stagedPath;
87+
this.setState(State.Ready({
88+
version: stagedCommit,
89+
productVersion: stagedVersion || '',
90+
changelogUrl: stagedChangelog || undefined,
91+
}, true, false));
92+
return;
93+
}
94+
} catch {
95+
// File doesn't exist — clean up stale storage
96+
}
97+
}
98+
99+
// Clean up stale keys if no valid staged update
100+
this.clearStagedUpdateStorage();
101+
}
102+
103+
private clearStagedUpdateStorage(): void {
104+
this.applicationStorageMainService.remove(STAGED_UPDATE_PATH_KEY, StorageScope.APPLICATION);
105+
this.applicationStorageMainService.remove(STAGED_UPDATE_COMMIT_KEY, StorageScope.APPLICATION);
106+
this.applicationStorageMainService.remove(STAGED_UPDATE_VERSION_KEY, StorageScope.APPLICATION);
107+
this.applicationStorageMainService.remove(STAGED_UPDATE_CHANGELOG_KEY, StorageScope.APPLICATION);
108+
}
109+
110+
private persistStagedUpdate(extractedAppPath: string, update: IUpdate): void {
111+
this.applicationStorageMainService.store(STAGED_UPDATE_PATH_KEY, extractedAppPath, StorageScope.APPLICATION, StorageTarget.MACHINE);
112+
this.applicationStorageMainService.store(STAGED_UPDATE_COMMIT_KEY, update.version!, StorageScope.APPLICATION, StorageTarget.MACHINE);
113+
this.applicationStorageMainService.store(STAGED_UPDATE_VERSION_KEY, update.productVersion!, StorageScope.APPLICATION, StorageTarget.MACHINE);
114+
if (update.changelogUrl) {
115+
this.applicationStorageMainService.store(STAGED_UPDATE_CHANGELOG_KEY, update.changelogUrl, StorageScope.APPLICATION, StorageTarget.MACHINE);
116+
}
117+
}
118+
69119
protected buildUpdateFeedUrl(quality: string, _commit: string, _options?: IUpdateURLOptions): string | undefined {
70120
const baseUrl = this.productService.updateUrl;
71121
if (!baseUrl) {
@@ -166,6 +216,15 @@ export class WorkstreamsUpdateService extends AbstractUpdateService implements I
166216
return;
167217
}
168218

219+
// Already staged this exact version — skip re-download
220+
if (this.stagedUpdatePath && this.state.type === StateType.Ready) {
221+
const stagedCommit = this.applicationStorageMainService.get(STAGED_UPDATE_COMMIT_KEY, StorageScope.APPLICATION);
222+
if (stagedCommit === manifest.version) {
223+
this.logService.info('workstreams-update#checkGitHubRelease - already staged', manifest.version);
224+
return;
225+
}
226+
}
227+
169228
this.logService.info('workstreams-update#checkGitHubRelease - update available', {
170229
current: this.productService.commit,
171230
new: manifest.version,
@@ -182,6 +241,9 @@ export class WorkstreamsUpdateService extends AbstractUpdateService implements I
182241
};
183242

184243
this.setState(State.AvailableForDownload(update));
244+
245+
// Auto-download in background — no user interaction needed
246+
this.doDownloadUpdate(State.AvailableForDownload(update) as AvailableForDownload);
185247
} catch (err) {
186248
this.logService.error('workstreams-update#checkGitHubRelease - error', err);
187249
this.setState(State.Idle(UpdateType.Archive, explicit ? String(err) : undefined));
@@ -244,6 +306,7 @@ export class WorkstreamsUpdateService extends AbstractUpdateService implements I
244306
}
245307

246308
this.stagedUpdatePath = extractedAppPath;
309+
this.persistStagedUpdate(extractedAppPath, update);
247310

248311
// Clean up the zip
249312
await Promises.rm(zipPath).catch(() => { /* ignore */ });
@@ -277,6 +340,8 @@ export class WorkstreamsUpdateService extends AbstractUpdateService implements I
277340
return;
278341
}
279342

343+
this.clearStagedUpdateStorage();
344+
280345
const currentAppPath = this.resolveCurrentAppPath();
281346
if (!currentAppPath) {
282347
this.logService.error('workstreams-update#doQuitAndInstall - could not resolve current app path');

apps/desktop/src/vs/workbench/browser/parts/orchestrator/media/orchestratorPart.css

Lines changed: 62 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -872,86 +872,98 @@
872872
line-height: 1.4;
873873
}
874874

875-
/* -- Update notification banner (bottom of orchestrator) -- */
875+
/* -- Update notification card (floating, bottom of orchestrator) -- */
876876

877877
.monaco-workbench .orchestrator-content .update-banner {
878-
flex-shrink: 0;
878+
position: absolute;
879+
bottom: 8px;
880+
left: 8px;
881+
right: 8px;
882+
z-index: 10;
879883
display: flex;
880884
flex-direction: column;
881-
gap: 8px;
882-
padding: 10px 12px;
883-
border-top: 1px solid var(--vscode-panel-border, rgba(255, 255, 255, 0.06));
884-
background: var(--vscode-sideBar-background);
885-
animation: updateBannerSlideIn 0.2s ease-out;
886-
}
887-
888-
@keyframes updateBannerSlideIn {
889-
from { opacity: 0; transform: translateY(8px); }
890-
to { opacity: 1; transform: translateY(0); }
891-
}
892-
893-
.monaco-workbench .orchestrator-content .update-banner-spin {
894-
animation: updateBannerSpin 1s linear infinite;
895-
}
896-
897-
@keyframes updateBannerSpin {
898-
from { transform: rotate(0deg); }
899-
to { transform: rotate(360deg); }
885+
gap: 14px;
886+
padding: 16px 18px 18px;
887+
border: 1px solid rgba(255, 255, 255, 0.07);
888+
border-radius: 10px;
889+
background: var(--vscode-editorWidget-background);
890+
box-shadow: 0 0 8px 2px var(--vscode-widget-shadow, rgba(0, 0, 0, 0.36));
891+
animation: updateBannerSlideIn 0.15s ease-out;
900892
}
901893

902-
.monaco-workbench .orchestrator-content .update-banner-text {
903-
font-size: 11px;
904-
color: var(--vscode-foreground);
894+
.monaco-workbench .orchestrator-content .update-banner-dismiss {
895+
position: absolute;
896+
top: 14px;
897+
right: 14px;
905898
display: flex;
906899
align-items: center;
907-
gap: 6px;
900+
justify-content: center;
901+
width: 16px;
902+
height: 16px;
903+
padding: 0;
904+
border: none;
905+
border-radius: 3px;
906+
background: transparent;
907+
color: var(--vscode-descriptionForeground);
908+
cursor: pointer;
909+
font-size: 12px;
910+
opacity: 0.4;
911+
transition: opacity 0.15s ease;
908912
}
909913

910-
.monaco-workbench .orchestrator-content .update-banner-icon {
911-
font-size: 14px;
912-
color: var(--vscode-notificationsInfoIcon-foreground, var(--vscode-focusBorder));
914+
.monaco-workbench .orchestrator-content .update-banner-dismiss:hover {
915+
opacity: 1;
913916
}
914917

915-
.monaco-workbench .orchestrator-content .update-banner-version {
916-
font-family: var(--monaco-monospace-font);
917-
font-size: 10px;
918-
color: var(--vscode-descriptionForeground);
918+
@keyframes updateBannerSlideIn {
919+
from { opacity: 0; transform: translateY(6px) scale(0.98); }
920+
to { opacity: 1; transform: translateY(0) scale(1); }
921+
}
922+
923+
.monaco-workbench .orchestrator-content .update-banner-description {
924+
font-size: 13px;
925+
line-height: 1.5;
926+
color: var(--vscode-editorWidget-foreground, var(--vscode-foreground));
927+
padding-right: 20px;
919928
}
920929

921930
.monaco-workbench .orchestrator-content .update-banner-actions {
922931
display: flex;
923-
gap: 6px;
932+
align-items: center;
933+
gap: 12px;
934+
margin-top: 2px;
924935
}
925936

926937
.monaco-workbench .orchestrator-content .update-banner-btn {
927-
flex: 1;
928-
padding: 4px 0;
929-
font-size: 11px;
938+
padding: 0;
930939
font-family: inherit;
931-
border: 1px solid var(--vscode-button-border, transparent);
932-
border-radius: 4px;
940+
border: none;
941+
background: transparent;
933942
cursor: pointer;
934-
text-align: center;
935943
transition: opacity 0.1s ease;
936944
}
937945

938-
.monaco-workbench .orchestrator-content .update-banner-btn.primary {
939-
background: var(--vscode-button-background);
940-
color: var(--vscode-button-foreground);
946+
.monaco-workbench .orchestrator-content .update-banner-btn.link {
947+
font-size: 12px;
948+
color: var(--vscode-descriptionForeground);
941949
}
942950

943-
.monaco-workbench .orchestrator-content .update-banner-btn.primary:hover {
944-
background: var(--vscode-button-hoverBackground);
951+
.monaco-workbench .orchestrator-content .update-banner-btn.link:hover {
952+
color: var(--vscode-foreground);
945953
}
946954

947-
.monaco-workbench .orchestrator-content .update-banner-btn.secondary {
948-
background: transparent;
949-
color: var(--vscode-foreground);
950-
border-color: var(--vscode-panel-border);
955+
.monaco-workbench .orchestrator-content .update-banner-btn.action {
956+
font-size: 12px;
957+
font-weight: 500;
958+
color: #111;
959+
background: #e8e8e8;
960+
border-radius: 6px;
961+
padding: 5px 16px;
962+
transition: background 0.1s ease;
951963
}
952964

953-
.monaco-workbench .orchestrator-content .update-banner-btn.secondary:hover {
954-
background: var(--vscode-toolbar-hoverBackground);
965+
.monaco-workbench .orchestrator-content .update-banner-btn.action:hover {
966+
background: #ffffff;
955967
}
956968

957969
/* -- Footer -- */

0 commit comments

Comments
 (0)