Auto-refresh the active tab's panels (silent, non-disturbing)
Local panels are fs.watched (LocalDirWatcher + setWatchedDirs -> pathRefresh);
remote panels poll every 3s. New refresh({silent}) skips the loading/busy
flags and touches no DOM, snapshotting sessionId+cwd to discard results if the
user navigated mid-flight. Only the active tab is live; switching tabs
refreshes the new pair. A future on/off setting is left as commented
breadcrumbs. Docs updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Injectable, computed, inject, signal, type WritableSignal } from '@angular/core';
|
||||
import { Injectable, computed, effect, inject, signal, type WritableSignal } from '@angular/core';
|
||||
import type { ColumnKey, ConnectionConfig, FileEntry, SavedConnection } from '@shared/types';
|
||||
import { fileKind } from './file-icons';
|
||||
import { FocusService } from './focus.service';
|
||||
@@ -108,19 +108,34 @@ export class PanelState {
|
||||
}
|
||||
}
|
||||
|
||||
/** Reload cwd, keeping cursor position and selection by name. */
|
||||
async refresh(): Promise<void> {
|
||||
private refreshing = false;
|
||||
|
||||
/**
|
||||
* Reload cwd, keeping cursor position and selection by name. `silent` (used by
|
||||
* auto-refresh) skips the loading/slowLoad flags — so no spinner, no dim
|
||||
* overlay, input never blocked — and touches no DOM, so scroll stays put.
|
||||
*/
|
||||
async refresh(opts?: { silent?: boolean }): Promise<void> {
|
||||
const silent = opts?.silent ?? false;
|
||||
// a silent refresh must never fight the user or overlap itself
|
||||
if (silent && (this.refreshing || this.busy())) return;
|
||||
this.refreshing = true;
|
||||
// snapshot what we're listing; a silent refresh has no busy-block, so the
|
||||
// user can navigate mid-flight — discard the result if they did
|
||||
const forSession = this.sessionId();
|
||||
const forDir = this.cwd();
|
||||
const keepCursorName = this.sorted()[this.cursor()]?.name;
|
||||
const keepSelected = this.selected();
|
||||
this.loading.set(true);
|
||||
const slowTimer = setTimeout(() => this.slowLoad.set(true), 300);
|
||||
if (!silent) this.loading.set(true);
|
||||
const slowTimer = silent ? null : setTimeout(() => this.slowLoad.set(true), 300);
|
||||
try {
|
||||
const res = await this.deps.rpc.request.listDir({
|
||||
sessionId: this.sessionId(),
|
||||
path: this.cwd(),
|
||||
sessionId: forSession,
|
||||
path: forDir,
|
||||
});
|
||||
if (this.sessionId() !== forSession || this.cwd() !== forDir) return;
|
||||
if (!res.ok) {
|
||||
this.deps.toast.error(res.error);
|
||||
if (!silent) this.deps.toast.error(res.error);
|
||||
return;
|
||||
}
|
||||
this.cwd.set(res.path);
|
||||
@@ -130,9 +145,12 @@ export class PanelState {
|
||||
const idx = this.sorted().findIndex((e) => e.name === keepCursorName);
|
||||
this.cursor.set(idx >= 0 ? idx : Math.min(this.cursor(), Math.max(0, this.sorted().length - 1)));
|
||||
} finally {
|
||||
clearTimeout(slowTimer);
|
||||
this.slowLoad.set(false);
|
||||
this.loading.set(false);
|
||||
if (slowTimer) clearTimeout(slowTimer);
|
||||
if (!silent) {
|
||||
this.slowLoad.set(false);
|
||||
this.loading.set(false);
|
||||
}
|
||||
this.refreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,12 +255,58 @@ export class PanelsService {
|
||||
}
|
||||
});
|
||||
|
||||
// an editor window saved a file — refresh panels showing that directory
|
||||
// a watched local dir changed, or an editor saved a file — silently
|
||||
// refresh every panel showing that directory (no spinner, view preserved)
|
||||
this.rpc.onPathRefresh(({ sessionId, dir }) => {
|
||||
for (const panel of this.allPanels()) {
|
||||
if (panel.sessionId() === sessionId && panel.cwd() === dir) void panel.refresh();
|
||||
if (panel.sessionId() === sessionId && panel.cwd() === dir) {
|
||||
void panel.refresh({ silent: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.setupAutoRefresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-refresh the active tab's panels without disturbing the view. Local
|
||||
* panels are watched instantly via fs.watch (backend pushes pathRefresh);
|
||||
* remote panels have no change notification, so they're polled. Only the
|
||||
* active tab is watched/polled; switching tabs refreshes the new one at once.
|
||||
*/
|
||||
private setupAutoRefresh(): void {
|
||||
// REMOTE_POLL_MS: also the cadence a future "auto-refresh interval" setting
|
||||
// would drive (see the commented breadcrumbs in settings).
|
||||
const REMOTE_POLL_MS = 3000;
|
||||
setInterval(() => {
|
||||
const ws = this.activeWorkspace();
|
||||
if (!ws) return;
|
||||
for (const panel of [ws.left, ws.right]) {
|
||||
if (panel.sessionId() !== 'local') void panel.refresh({ silent: true });
|
||||
}
|
||||
}, REMOTE_POLL_MS);
|
||||
|
||||
// register the active tab's LOCAL dirs for fs.watch; re-runs when the active
|
||||
// tab changes or either panel navigates/(dis)connects
|
||||
effect(() => {
|
||||
const ws = this.activeWorkspace();
|
||||
const dirs = ws
|
||||
? [ws.left, ws.right]
|
||||
.filter((p) => p.sessionId() === 'local' && p.cwd())
|
||||
.map((p) => p.cwd())
|
||||
: [];
|
||||
void this.rpc.request.setWatchedDirs({ dirs: [...new Set(dirs)] });
|
||||
});
|
||||
|
||||
// switching tabs shows fresh data immediately instead of waiting a poll tick
|
||||
let lastActive = this.activeWorkspaceId();
|
||||
effect(() => {
|
||||
const id = this.activeWorkspaceId();
|
||||
if (id === lastActive) return;
|
||||
lastActive = id;
|
||||
const ws = this.activeWorkspace();
|
||||
if (ws) for (const panel of [ws.left, ws.right]) void panel.refresh({ silent: true });
|
||||
});
|
||||
}
|
||||
|
||||
// ---- panels of the active workspace ----
|
||||
|
||||
@@ -16,6 +16,11 @@ export class SettingsService {
|
||||
readonly autoClearCompleted = signal(DEFAULT_SETTINGS.autoClearCompleted);
|
||||
readonly editor = signal<AppSettings['editor']>(DEFAULT_SETTINGS.editor);
|
||||
readonly editorApp = signal(DEFAULT_SETTINGS.editorApp);
|
||||
// Auto-refresh is always on for now (see panels.service). To make it
|
||||
// user-controllable: uncomment the field in types.ts, then here add
|
||||
// readonly autoRefresh = signal(DEFAULT_SETTINGS.autoRefresh);
|
||||
// load it in the constructor, add setAutoRefresh(), include it in settings(),
|
||||
// gate the poll/watch in panels.service on it, and add the tab-bar row below.
|
||||
|
||||
private readonly rpc = inject(RpcService);
|
||||
|
||||
|
||||
@@ -342,6 +342,23 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auto-refresh is always on for now. To expose a toggle, uncomment
|
||||
the breadcrumbs in types.ts + settings.service, then this row:
|
||||
<div class="mt-2.5 flex items-center justify-between gap-2">
|
||||
<span class="text-xs text-muted-foreground">Auto-refresh</span>
|
||||
<div class="flex rounded-md border border-border p-0.5">
|
||||
<button (click)="settings.setAutoRefresh(true)" class="rounded px-2 py-0.5 text-[11px]"
|
||||
[class.bg-accent]="settings.autoRefresh()"
|
||||
[class.text-foreground]="settings.autoRefresh()"
|
||||
[class.text-muted-foreground]="!settings.autoRefresh()">On</button>
|
||||
<button (click)="settings.setAutoRefresh(false)" class="rounded px-2 py-0.5 text-[11px]"
|
||||
[class.bg-accent]="!settings.autoRefresh()"
|
||||
[class.text-foreground]="!settings.autoRefresh()"
|
||||
[class.text-muted-foreground]="settings.autoRefresh()">Off</button>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<!-- about footer -->
|
||||
<button
|
||||
(click)="showAbout()"
|
||||
|
||||
Reference in New Issue
Block a user