diff --git a/README.md b/README.md
index 0ab8784..5eeb718 100644
--- a/README.md
+++ b/README.md
@@ -25,10 +25,8 @@ remote server over SSH/SFTP, so you can move files **local↔remote**,
(MB/s or Mbps), persisted across restarts.
- On macOS the tab bar is fused with the window titlebar; other platforms keep
a standard titlebar.
-- **Menu-bar (tray) icon**: template icon that adapts to light/dark; during
- transfers it fades and fills an aggregate progress bar (delivered/total bytes
- of the whole queue). Its menu lists your tabs with live per-tab transfer
- info — click one to jump to it — plus Show and Quit.
+- **Menu-bar (tray) icon**: template icon that adapts to light/dark; click it
+ to bring the app to the front.
- **Edit files** (F4 / toolbar / File menu): a built-in editor works on local
and remote files directly; or pick an external app (Options → Editor →
Custom → Choose…). External edits of remote files download to a temp folder,
diff --git a/assets/tray/progress/p0.png b/assets/tray/progress/p0.png
deleted file mode 100644
index 7b79961..0000000
Binary files a/assets/tray/progress/p0.png and /dev/null differ
diff --git a/assets/tray/progress/p10.png b/assets/tray/progress/p10.png
deleted file mode 100644
index 18c4dfc..0000000
Binary files a/assets/tray/progress/p10.png and /dev/null differ
diff --git a/assets/tray/progress/p100.png b/assets/tray/progress/p100.png
deleted file mode 100644
index b9fd386..0000000
Binary files a/assets/tray/progress/p100.png and /dev/null differ
diff --git a/assets/tray/progress/p15.png b/assets/tray/progress/p15.png
deleted file mode 100644
index 982ce19..0000000
Binary files a/assets/tray/progress/p15.png and /dev/null differ
diff --git a/assets/tray/progress/p20.png b/assets/tray/progress/p20.png
deleted file mode 100644
index 2fb9287..0000000
Binary files a/assets/tray/progress/p20.png and /dev/null differ
diff --git a/assets/tray/progress/p25.png b/assets/tray/progress/p25.png
deleted file mode 100644
index 4b0e5f1..0000000
Binary files a/assets/tray/progress/p25.png and /dev/null differ
diff --git a/assets/tray/progress/p30.png b/assets/tray/progress/p30.png
deleted file mode 100644
index b4fbd46..0000000
Binary files a/assets/tray/progress/p30.png and /dev/null differ
diff --git a/assets/tray/progress/p35.png b/assets/tray/progress/p35.png
deleted file mode 100644
index 1b6cb88..0000000
Binary files a/assets/tray/progress/p35.png and /dev/null differ
diff --git a/assets/tray/progress/p40.png b/assets/tray/progress/p40.png
deleted file mode 100644
index 18b3307..0000000
Binary files a/assets/tray/progress/p40.png and /dev/null differ
diff --git a/assets/tray/progress/p45.png b/assets/tray/progress/p45.png
deleted file mode 100644
index 1cba41b..0000000
Binary files a/assets/tray/progress/p45.png and /dev/null differ
diff --git a/assets/tray/progress/p5.png b/assets/tray/progress/p5.png
deleted file mode 100644
index 1cf4c40..0000000
Binary files a/assets/tray/progress/p5.png and /dev/null differ
diff --git a/assets/tray/progress/p50.png b/assets/tray/progress/p50.png
deleted file mode 100644
index f193657..0000000
Binary files a/assets/tray/progress/p50.png and /dev/null differ
diff --git a/assets/tray/progress/p55.png b/assets/tray/progress/p55.png
deleted file mode 100644
index 2d14b62..0000000
Binary files a/assets/tray/progress/p55.png and /dev/null differ
diff --git a/assets/tray/progress/p60.png b/assets/tray/progress/p60.png
deleted file mode 100644
index c66d77b..0000000
Binary files a/assets/tray/progress/p60.png and /dev/null differ
diff --git a/assets/tray/progress/p65.png b/assets/tray/progress/p65.png
deleted file mode 100644
index 182e845..0000000
Binary files a/assets/tray/progress/p65.png and /dev/null differ
diff --git a/assets/tray/progress/p70.png b/assets/tray/progress/p70.png
deleted file mode 100644
index a5767d3..0000000
Binary files a/assets/tray/progress/p70.png and /dev/null differ
diff --git a/assets/tray/progress/p75.png b/assets/tray/progress/p75.png
deleted file mode 100644
index 7b380af..0000000
Binary files a/assets/tray/progress/p75.png and /dev/null differ
diff --git a/assets/tray/progress/p80.png b/assets/tray/progress/p80.png
deleted file mode 100644
index f53be36..0000000
Binary files a/assets/tray/progress/p80.png and /dev/null differ
diff --git a/assets/tray/progress/p85.png b/assets/tray/progress/p85.png
deleted file mode 100644
index 26b85d8..0000000
Binary files a/assets/tray/progress/p85.png and /dev/null differ
diff --git a/assets/tray/progress/p90.png b/assets/tray/progress/p90.png
deleted file mode 100644
index b5f51de..0000000
Binary files a/assets/tray/progress/p90.png and /dev/null differ
diff --git a/assets/tray/progress/p95.png b/assets/tray/progress/p95.png
deleted file mode 100644
index eabadd7..0000000
Binary files a/assets/tray/progress/p95.png and /dev/null differ
diff --git a/scripts/gen-icons.ts b/scripts/gen-icons.ts
index cb73429..273bb21 100644
--- a/scripts/gen-icons.ts
+++ b/scripts/gen-icons.ts
@@ -1,7 +1,7 @@
// Regenerate every raster icon asset from the SVG sources (dev-time only).
// bun scripts/gen-icons.ts
// Produces: assets/icon.png (1024), assets/icon.iconset/* (macOS app icon),
-// assets/tray/tray-44-template.png, assets/tray/progress/p{0..100}.png.
+// assets/tray/tray-44-template.png.
import { mkdir } from "node:fs/promises";
import path from "node:path";
import { Resvg } from "@resvg/resvg-js";
@@ -31,23 +31,4 @@ for (const size of [16, 32, 128, 256, 512]) {
const trayGlyph = await Bun.file(path.join(assets, "tray", "tray-glyph.svg")).text();
await writePng(path.join(assets, "tray", "tray-44-template.png"), trayGlyph, 44);
-// ---- tray progress frames: faded arrows + centered bar filled to pct ----
-function progressSvg(pct: number): string {
- const trackX = 4;
- const trackW = 16;
- const fillW = (trackW * pct) / 100;
- return ``;
-}
-
-for (let pct = 0; pct <= 100; pct += 5) {
- await writePng(path.join(assets, "tray", "progress", `p${pct}.png`), progressSvg(pct), 44);
-}
-
-console.log("icons regenerated: app iconset, tray template, 21 progress frames");
+console.log("icons regenerated: app iconset + tray template");
diff --git a/src/bun/index.ts b/src/bun/index.ts
index 0ff2ce0..997b45b 100644
--- a/src/bun/index.ts
+++ b/src/bun/index.ts
@@ -38,7 +38,6 @@ const edits = new EditManager(registry, engine, settings);
const events = {
sessionClosed: (_info: { sessionId: string; reason: string }) => {},
connectionsChanged: () => {},
- workspacesSync: (_sync: import("../shared/rpc").WorkspaceSyncInfo) => {},
};
const rpc = createAppRPC({ registry, engine, store, settings, edits, events });
const url = await getMainViewUrl();
@@ -65,16 +64,11 @@ const win = new BrowserWindow({
rpc,
});
-const tray = new AppTray(win, engine);
-tray.onAction = (action) => rpc.send.menuAction({ action });
+new AppTray(win);
-engine.onUpdate = (snapshot) => {
- rpc.send.transferUpdate(snapshot);
- tray.updateProgress();
-};
+engine.onUpdate = (snapshot) => rpc.send.transferUpdate(snapshot);
events.sessionClosed = (info) => rpc.send.sessionClosed(info);
events.connectionsChanged = () => void applyApplicationMenu(store);
-events.workspacesSync = (sync) => tray.updateTabs(sync);
void settings.load().then((s) => engine.setConcurrency(effectiveConcurrency(s)));
void applyApplicationMenu(store);
diff --git a/src/bun/rpc-handlers.ts b/src/bun/rpc-handlers.ts
index bf3262e..fcf367a 100644
--- a/src/bun/rpc-handlers.ts
+++ b/src/bun/rpc-handlers.ts
@@ -1,5 +1,5 @@
import { BrowserView, Utils } from "electrobun/bun";
-import type { AppRPC, WorkspaceSyncInfo } from "../shared/rpc";
+import type { AppRPC } from "../shared/rpc";
import { effectiveConcurrency, type Result } from "../shared/types";
import { readFileText, writeFileText, type EditManager } from "./edits";
import type { SessionRegistry } from "./fs/registry";
@@ -23,7 +23,6 @@ export interface AppDeps {
events: {
sessionClosed: (info: { sessionId: string; reason: string }) => void;
connectionsChanged: () => void;
- workspacesSync: (sync: WorkspaceSyncInfo) => void;
};
}
@@ -203,9 +202,6 @@ export function createAppRPC({ registry, engine, store, settings, edits, events
return {};
}),
},
- messages: {
- workspacesSync: (sync) => events.workspacesSync(sync),
- },
},
});
}
diff --git a/src/bun/transfers/engine.ts b/src/bun/transfers/engine.ts
index 6563a54..db9a0c0 100644
--- a/src/bun/transfers/engine.ts
+++ b/src/bun/transfers/engine.ts
@@ -146,35 +146,6 @@ export class TransferEngine {
if (job && TERMINAL.includes(job.snapshot.state)) this.jobs.delete(jobId);
}
- /** Live totals over every non-terminal job (drives the tray progress icon). */
- aggregateProgress(): {
- activeCount: number;
- doneBytes: number;
- totalBytes: number;
- bySession: Record;
- } {
- const result = {
- activeCount: 0,
- doneBytes: 0,
- totalBytes: 0,
- bySession: {} as Record,
- };
- for (const job of this.jobs.values()) {
- const snap = job.snapshot;
- if (TERMINAL.includes(snap.state)) continue;
- result.activeCount++;
- result.doneBytes += snap.doneBytes;
- result.totalBytes += snap.totalBytes;
- for (const sessionId of new Set([snap.srcSessionId, snap.dstSessionId])) {
- const entry = (result.bySession[sessionId] ??= { count: 0, done: 0, total: 0 });
- entry.count++;
- entry.done += snap.doneBytes;
- entry.total += snap.totalBytes;
- }
- }
- return result;
- }
-
/** Fail every non-terminal job that touches a dead session. */
failSession(sessionId: string, reason: string): void {
for (const job of this.jobs.values()) {
diff --git a/src/bun/tray.ts b/src/bun/tray.ts
index 4a97d45..167d2a0 100644
--- a/src/bun/tray.ts
+++ b/src/bun/tray.ts
@@ -1,130 +1,22 @@
import { Tray, type BrowserWindow } from "electrobun/bun";
-import { BRANDING } from "../shared/branding";
-import type { WorkspaceSyncInfo } from "../shared/rpc";
-import type { TransferEngine } from "./transfers/engine";
-
-const BASE_IMAGE = "views://assets/tray/tray-44-template.png";
-const PROGRESS_IMAGE = (pct: number) => `views://assets/tray/progress/p${pct}.png`;
-const REFRESH_THROTTLE_MS = 500;
/**
- * Menu-bar icon: the app glyph when idle; while transfers run the icon swaps
- * to pre-rendered frames (faded arrows + fill bar = queue doneBytes/totalBytes).
- * The menu lists the tabs (synced from the webview) with per-tab job info.
+ * Menu-bar icon: the app glyph as a macOS template image (adapts to
+ * light/dark). Clicking it brings the app window to the front. No menu is ever
+ * set, so clicks arrive directly; no setImage swaps, so the template
+ * treatment from construction always applies.
*/
export class AppTray {
private tray: Tray;
- private tabs: WorkspaceSyncInfo = { workspaces: [], activeId: 0 };
- private lastRefresh = 0;
- private refreshTimer: ReturnType | null = null;
- private currentImage = BASE_IMAGE;
- /** Forwards tray menu actions the UI must handle (activate-tab:). */
- onAction: (action: string) => void = () => {};
- constructor(
- private win: BrowserWindow,
- private engine: TransferEngine,
- ) {
+ constructor(win: BrowserWindow) {
this.tray = new Tray({
title: "",
- image: BASE_IMAGE,
+ image: "views://assets/tray/tray-44-template.png",
template: true,
width: 22,
height: 22,
});
-
- this.tray.on("tray-clicked", (event) => {
- const { action } = (event as { data: { action: string } }).data;
- if (!action) return; // icon click with a menu set -> macOS opens the menu
- if (action === "show-app") {
- this.win.activate();
- } else if (action === "quit") {
- process.exit(0); // electrobun's clean-quit override
- } else if (action.startsWith("activate-tab:")) {
- this.win.activate();
- this.onAction(action);
- }
- });
-
- this.rebuildMenu();
- }
-
- updateTabs(sync: WorkspaceSyncInfo): void {
- this.tabs = sync;
- this.scheduleRefresh();
- }
-
- /** Called on every engine update; throttled internally. */
- updateProgress(): void {
- this.scheduleRefresh();
- }
-
- private scheduleRefresh(): void {
- const now = Date.now();
- const wait = Math.max(0, REFRESH_THROTTLE_MS - (now - this.lastRefresh));
- if (this.refreshTimer) return;
- this.refreshTimer = setTimeout(() => {
- this.refreshTimer = null;
- this.lastRefresh = Date.now();
- this.refresh();
- }, wait);
- }
-
- private refresh(): void {
- const progress = this.engine.aggregateProgress();
-
- // icon: nearest 5% frame while transferring, base glyph when idle
- let image = BASE_IMAGE;
- if (progress.activeCount > 0 && progress.totalBytes > 0) {
- const pct = Math.round(((progress.doneBytes / progress.totalBytes) * 100) / 5) * 5;
- image = PROGRESS_IMAGE(Math.max(0, Math.min(100, pct)));
- }
- if (image !== this.currentImage) {
- this.currentImage = image;
- this.tray.setImage(image);
- }
-
- this.rebuildMenu(progress);
- }
-
- private rebuildMenu(progress = this.engine.aggregateProgress()): void {
- const items: Parameters[0] = [];
-
- for (const ws of this.tabs.workspaces) {
- const jobs = ws.sessionIds
- .map((id) => progress.bySession[id])
- .filter((v): v is NonNullable => !!v);
- const count = jobs.reduce((n, j) => n + j.count, 0);
- const total = jobs.reduce((n, j) => n + j.total, 0);
- const done = jobs.reduce((n, j) => n + j.done, 0);
- const suffix = count
- ? ` — ${count} transfer${count === 1 ? "" : "s"} · ${total ? Math.round((done / total) * 100) : 0}%`
- : "";
- items.push({
- type: "normal",
- label: `${ws.name}${suffix}`,
- action: `activate-tab:${ws.id}`,
- checked: ws.id === this.tabs.activeId,
- });
- }
-
- if (progress.activeCount > 0) {
- items.push({ type: "divider" });
- const pct = progress.totalBytes
- ? Math.round((progress.doneBytes / progress.totalBytes) * 100)
- : 0;
- items.push({
- type: "normal",
- label: `${progress.activeCount} active transfer${progress.activeCount === 1 ? "" : "s"} · ${pct}%`,
- action: "noop",
- enabled: false,
- });
- }
-
- items.push({ type: "divider" });
- items.push({ type: "normal", label: `Show ${BRANDING.appName}`, action: "show-app" });
- items.push({ type: "normal", label: "Quit", action: "quit" });
-
- this.tray.setMenu(items);
+ this.tray.on("tray-clicked", () => win.activate());
}
}
diff --git a/src/shared/rpc.ts b/src/shared/rpc.ts
index ce9de9b..3c2ff18 100644
--- a/src/shared/rpc.ts
+++ b/src/shared/rpc.ts
@@ -15,13 +15,7 @@ import type {
// pulling in bun-only type modules.
//
// bun.requests = calls the webview makes, handled in src/bun/rpc-handlers.ts
-// bun.messages = fire-and-forget pushes from the webview (tray state)
// webview.messages = push events from bun, handled in ui rpc.service.ts
-
-export interface WorkspaceSyncInfo {
- workspaces: { id: number; name: string; remote: boolean; sessionIds: string[] }[];
- activeId: number;
-}
export type AppRPC = {
bun: {
requests: {
@@ -103,9 +97,7 @@ export type AppRPC = {
};
clearEditLeftovers: { params: Record; response: Result };
};
- messages: {
- workspacesSync: WorkspaceSyncInfo;
- };
+ messages: Record;
};
webview: {
requests: Record;
diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts
index c63fb78..a45f3c1 100644
--- a/ui/src/app/app.ts
+++ b/ui/src/app/app.ts
@@ -67,13 +67,8 @@ export class App {
});
}
- /** Native application-menu / tray actions forwarded from the bun process. */
+ /** Native application-menu actions forwarded from the bun process. */
private handleMenuAction(action: string): void {
- if (action.startsWith('activate-tab:')) {
- const id = Number(action.slice('activate-tab:'.length));
- if (Number.isFinite(id)) this.panels.setActiveWorkspace(id);
- return;
- }
if (action.startsWith('connect:')) {
const id = action.slice('connect:'.length);
const connection = this.connections.connections().find((c) => c.id === id);
diff --git a/ui/src/app/panels.service.ts b/ui/src/app/panels.service.ts
index a864536..d5785f1 100644
--- a/ui/src/app/panels.service.ts
+++ b/ui/src/app/panels.service.ts
@@ -1,4 +1,4 @@
-import { Injectable, computed, effect, signal, type WritableSignal } from '@angular/core';
+import { Injectable, computed, signal, type WritableSignal } from '@angular/core';
import type { ConnectionConfig, FileEntry, SavedConnection } from '@shared/types';
import { RpcService } from './rpc.service';
import { ToastService } from './toast.service';
@@ -180,21 +180,6 @@ export class PanelsService {
) {
this.newWorkspace();
- // keep the bun-side tray informed about tabs (names, sessions, active)
- if (this.rpc.available) {
- effect(() => {
- this.rpc.send.workspacesSync({
- workspaces: this.workspaces().map((ws) => ({
- id: ws.id,
- name: this.workspaceName(ws),
- remote: this.workspaceIsRemote(ws),
- sessionIds: [ws.left.sessionId(), ws.right.sessionId()],
- })),
- activeId: this.activeWorkspaceId(),
- });
- });
- }
-
// a session died on its own — reset every panel bound to it, in any tab
this.rpc.onSessionClosed((event) => {
for (const workspace of this.workspaces()) {
diff --git a/ui/src/app/rpc.service.ts b/ui/src/app/rpc.service.ts
index cdd00cd..4fdd40d 100644
--- a/ui/src/app/rpc.service.ts
+++ b/ui/src/app/rpc.service.ts
@@ -87,20 +87,4 @@ export class RpcService {
});
}
- /** Fire-and-forget messages to the bun process (e.g. workspacesSync). */
- get send() {
- const rpc = this.rpc;
- if (!rpc) throw new Error('RPC is only available inside the desktop app');
- const ready = this.ready;
- return new Proxy({} as (typeof rpc)['send'], {
- get:
- (_target, message: string) =>
- (payload?: unknown) => {
- void ready.then(() => {
- const messages = rpc.send as unknown as Record void>;
- messages[message](payload);
- });
- },
- });
- }
}