v1.1: fix transfer freeze, session tabs, per-panel toolbars, status bar

Freeze root cause: TransfersService consumed transferUpdate through an effect
that both read and wrote the jobMap signal — the first progress message spun
the effect into an infinite re-run loop, pegging the webview thread while the
copy completed fine bun-side. Push messages now flow through plain callback
registries on RpcService (also stops signal coalescing from dropping progress
snapshots). Verified live: a 60MB boot-time self-test copy streamed progress
into the running UI and the post-completion panel refreshes arrived.

- Tabs: workspaces (left+right PanelState pair each) in PanelsService; tab bar
  replaces the header; sessions persist in background tabs; Cmd+T/Cmd+W/Ctrl+Tab;
  closing a tab disconnects its sessions; last tab can't close
- Per-panel toolbar (top, icon-only): direction-explicit copy (-> / <-),
  rename, new folder/file, chmod, delete; panel footers removed
- VSCode-style status bar: transfer summary (spinner/percent/queued/failures,
  click to expand the job list) + active panel info; function bar removed
- transfer details list is collapsible, auto-expands on failure

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jokin
2026-07-11 14:50:19 +02:00
co-authored by Claude Fable 5
parent 76b9b621a2
commit 9eae6c58dc
12 changed files with 410 additions and 158 deletions
+18 -6
View File
@@ -34,6 +34,8 @@ export class FilePanel {
readonly panel = computed(() => this.panels.get(this.side()));
readonly isActive = computed(() => this.panels.activeSide() === this.side());
readonly isRemote = computed(() => this.panel().sessionId() !== 'local');
/** The copy button points toward the other panel. */
readonly copyIcon = computed(() => (this.side() === 'left' ? 'mynaArrowRight' : 'mynaArrowLeft'));
readonly pathDraft = signal('');
readonly menuOpen = signal(false);
@@ -55,7 +57,17 @@ export class FilePanel {
}
activate(): void {
this.panels.activeSide.set(this.side());
this.panels.setActiveSide(this.side());
}
copyToOther(): void {
this.activate();
void this.transfers.copyToOtherPanel(this.side());
}
run(action: 'rename' | 'mkdir' | 'createFile' | 'chmod' | 'delete'): void {
this.activate();
this.actions[action](this.panel());
}
pickLocal(): void {
@@ -129,7 +141,7 @@ export class FilePanel {
if (key === 'Tab') {
event.preventDefault();
this.panels.activeSide.set(this.panels.otherSide(this.side()));
this.panels.setActiveSide(this.panels.otherSide(this.side()));
return;
}
if ((event.metaKey || event.ctrlKey) && key.toLowerCase() === 'a') {
@@ -171,17 +183,17 @@ export class FilePanel {
panel.up();
break;
case 'F2':
this.actions.rename();
this.actions.rename(panel);
break;
case 'F5':
void this.transfers.copyToOtherPanel();
void this.transfers.copyToOtherPanel(this.side());
break;
case 'F7':
this.actions.mkdir();
this.actions.mkdir(panel);
break;
case 'F8':
case 'Delete':
this.actions.delete();
this.actions.delete(panel);
break;
default:
return; // let unhandled keys through