Tray icon with live transfer progress and tabs menu

- AppTray: template menu-bar icon (22pt, adapts to light/dark). While
  transfers run it swaps to pre-rendered frames — faded arrows + a fill bar
  showing aggregate doneBytes/totalBytes of the queue (nearest 5%, throttled
  500ms); back to the glyph when idle. No runtime image encoding.
- Menu: one entry per tab (checkmark on the active one) with live per-tab
  transfer count and percentage (jobs mapped via session ids), an overall
  progress row while transferring, Show <app>, Quit. Selecting a tab
  activates the window and switches to it.
- First use of webview->bun messages: PanelsService pushes workspacesSync
  (tab names, sessions, active id) via a new ready-gated send proxy; verified
  end-to-end in the running app.
- engine.aggregateProgress(): totals + per-session breakdown over non-terminal
  jobs (unit-verified idle/mid-transfer/idle).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jokin
2026-07-11 21:41:51 +02:00
co-authored by Claude Fable 5
parent c218cc2104
commit 42e5e5bb9a
10 changed files with 228 additions and 6 deletions
+29
View File
@@ -146,6 +146,35 @@ 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<string, { count: number; done: number; total: number }>;
} {
const result = {
activeCount: 0,
doneBytes: 0,
totalBytes: 0,
bySession: {} as Record<string, { count: number; done: number; total: number }>,
};
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()) {