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 apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@tauri-apps/plugin-dialog": "^2.0.0",
"@tauri-apps/plugin-fs": "^2.0.0",
"@tauri-apps/plugin-opener": "^2.0.0",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-shell": "^2.0.0",
"@tauri-apps/plugin-updater": "^2.0.0",
"react": "^18.3.0",
Expand Down
11 changes: 11 additions & 0 deletions apps/desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ tauri-plugin-fs = "2"
tauri-plugin-opener = "2"
tauri-plugin-shell = "2"
tauri-plugin-updater = "2"
tauri-plugin-process = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "1"
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"fs:default",
"opener:default",
"shell:default",
"updater:default"
"updater:default",
"process:default",
"process:allow-restart"
]
}
1 change: 1 addition & 0 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub fn run() {
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init())
.invoke_handler(tauri::generate_handler![
get_app_info,
read_credentials,
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@
},
"plugins": {
"updater": {
"active": false,
"active": true,
"endpoints": [
"https://github.com/oratis/deepcode/releases/latest/download/latest.json"
],
"dialog": false,
"pubkey": ""
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDk0MDMzRUQ0RkVFNUREODUKUldTRjNlWCsxRDREbEttVHEwUEplK1FKbnRCakpGb3dVWTYveFdxSmxwK2ZROWFZcW1kYzZMSGcK"
}
}
}
12 changes: 10 additions & 2 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { useEffect, useState } from 'react';
import { Nav, type ScreenName } from './components/Nav.js';
import { UpdateBanner } from './components/UpdateBanner.js';
import { onUpdateDownloaded, startUpdaterPolling } from './lib/updater.js';
import { AboutScreen } from './screens/About.js';
import { ChatScreen } from './screens/Chat.js';
import { MCPManagerScreen } from './screens/MCPManager.js';
Expand All @@ -26,8 +27,15 @@ export function App(): JSX.Element {
useEffect(() => {
void window.deepcode.version().then(setVersion);
void window.deepcode.creds.load().then((c) => setHasKey(c.hasKey));
const off = window.deepcode.onUpdateDownloaded((info) => setUpdate(info));
return () => off();
const offShim = window.deepcode.onUpdateDownloaded((info) => setUpdate(info));
// Also subscribe to the real Tauri updater (so even if the shim
// surface drifts, the banner still fires).
const offReal = onUpdateDownloaded((info) => setUpdate(info));
startUpdaterPolling();
return () => {
offShim();
offReal();
};
}, []);

if (hasKey === null) {
Expand Down
33 changes: 20 additions & 13 deletions apps/desktop/src/components/UpdateBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// "Relaunch to update vX.Y.Z" banner — fires when electron-updater has
// downloaded a new release. Click → app.relaunch() (host wires this in M6-rest).
// "Relaunch to update vX.Y.Z" banner — fires when tauri-plugin-updater has
// downloaded a new release. Clicking Relaunch calls tauri-plugin-process.relaunch().
// Spec: docs/VISUAL_DESIGN.html screen #11
// Milestone: M6 skeleton

import { useState } from 'react';
import { relaunchNow } from '../lib/updater.js';
import type { UpdateInfo } from '../types/global.js';

interface BannerProps {
Expand All @@ -12,22 +12,29 @@ interface BannerProps {

export function UpdateBanner({ info }: BannerProps): JSX.Element | null {
const [dismissed, setDismissed] = useState(false);
const [relaunching, setRelaunching] = useState(false);
if (dismissed) return null;

async function handleRelaunch(): Promise<void> {
setRelaunching(true);
try {
await relaunchNow();
} catch (err) {
console.error('relaunch failed:', err);
setRelaunching(false);
}
}

return (
<div className="flex items-center justify-between border-b border-border bg-accent/10 px-4 py-2 text-sm">
<span>
DeepCode v{info.version} is ready to install. Relaunch to update.
</span>
<span>DeepCode v{info.version} is ready to install. Relaunch to update.</span>
<div className="flex gap-2">
<button
className="rounded bg-accent px-3 py-1 text-xs font-medium text-bg"
onClick={() => {
// The renderer can't relaunch directly — main process listens for
// this and calls app.relaunch(). Wiring in M6-rest.
window.location.reload();
}}
className="rounded bg-accent px-3 py-1 text-xs font-medium text-bg disabled:opacity-50"
onClick={handleRelaunch}
disabled={relaunching}
>
Relaunch now
{relaunching ? 'Relaunching…' : 'Relaunch now'}
</button>
<button
className="rounded px-3 py-1 text-xs text-muted"
Expand Down
84 changes: 84 additions & 0 deletions apps/desktop/src/lib/updater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Tauri auto-updater wiring.
// Spec: docs/DEVELOPMENT_PLAN.md §4b
//
// On app start, we silently poll the GitHub Releases feed. When a newer
// release is available, we download it in the background and fire an
// `update-downloaded` event the renderer listens for (UpdateBanner UI).
//
// Privacy: only a single HEAD-equivalent JSON fetch to the GitHub
// releases endpoint. No telemetry beyond that.

import { check, type Update } from '@tauri-apps/plugin-updater';

type UpdateInfoCb = (info: { version: string; releaseNotes?: string }) => void;

const listeners: UpdateInfoCb[] = [];

/** Subscribe to update-downloaded events. Returns an unsubscribe fn. */
export function onUpdateDownloaded(cb: UpdateInfoCb): () => void {
listeners.push(cb);
return () => {
const i = listeners.indexOf(cb);
if (i >= 0) listeners.splice(i, 1);
};
}

function emit(info: { version: string; releaseNotes?: string }): void {
for (const l of listeners) {
try {
l(info);
} catch {
/* listeners are isolated */
}
}
}

/** Begin background update polling. Safe to call multiple times — only the
* first call actually polls. */
let pollStarted = false;
export function startUpdaterPolling(): void {
if (pollStarted) return;
pollStarted = true;
void checkAndDownloadOnce().catch((err) => {
// Silent — offline / no releases yet shouldn't bother the user.
console.warn('[updater]', (err as Error).message);
});
}

async function checkAndDownloadOnce(): Promise<void> {
let update: Update | null = null;
try {
update = await check();
} catch (err) {
// Network error / endpoint 404 → expected during early ship phase.
console.warn('[updater] check failed:', (err as Error).message);
return;
}
if (!update?.available) {
console.info('[updater] up to date');
return;
}
console.info(`[updater] new version available: ${update.version}`);
// Stream-download with progress; finishOnLoad triggers immediately.
let downloaded = 0;
await update.downloadAndInstall((event) => {
if (event.event === 'Started') {
console.info(`[updater] downloading ${event.data.contentLength ?? 0} bytes`);
} else if (event.event === 'Progress') {
downloaded += event.data.chunkLength;
} else if (event.event === 'Finished') {
console.info(`[updater] downloaded ${downloaded} bytes — ready`);
emit({
version: update?.version ?? '',
releaseNotes: update?.body ?? undefined,
});
}
});
}

/** Trigger app relaunch (after update is installed). Called from the
* UpdateBanner "Relaunch now" button. */
export async function relaunchNow(): Promise<void> {
const { relaunch } = await import('@tauri-apps/plugin-process');
await relaunch();
}
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading