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:
@@ -0,0 +1,64 @@
|
||||
import { type FSWatcher, watch } from "node:fs";
|
||||
|
||||
const DEBOUNCE_MS = 150;
|
||||
|
||||
/**
|
||||
* Watches a set of local directories with node's fs.watch and fires onChange
|
||||
* (debounced per dir — fs.watch emits bursts) when their contents change. Used
|
||||
* to auto-refresh the active tab's local panels instantly; SFTP has no
|
||||
* equivalent, so remote panels are polled UI-side instead.
|
||||
*
|
||||
* onChange is late-bound (assigned after the RPC exists, like AppDeps.events)
|
||||
* so it can push pathRefresh to the main webview.
|
||||
*/
|
||||
export class LocalDirWatcher {
|
||||
onChange: (dir: string) => void = () => {};
|
||||
private readonly handles = new Map<string, FSWatcher>();
|
||||
private readonly debounces = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
/** Replace the watched set: drop handles no longer wanted, open new ones. */
|
||||
setWatched(dirs: string[]): void {
|
||||
const wanted = new Set(dirs);
|
||||
for (const [dir, handle] of this.handles) {
|
||||
if (!wanted.has(dir)) {
|
||||
handle.close();
|
||||
this.handles.delete(dir);
|
||||
}
|
||||
}
|
||||
for (const dir of wanted) {
|
||||
if (this.handles.has(dir)) continue;
|
||||
try {
|
||||
const handle = watch(dir, () => this.fire(dir));
|
||||
// a watched dir that gets deleted/unmounted errors — just drop it
|
||||
handle.on("error", () => this.drop(dir));
|
||||
this.handles.set(dir, handle);
|
||||
} catch {
|
||||
// dir vanished between listing and watching — ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fire(dir: string): void {
|
||||
const pending = this.debounces.get(dir);
|
||||
if (pending) clearTimeout(pending);
|
||||
this.debounces.set(
|
||||
dir,
|
||||
setTimeout(() => {
|
||||
this.debounces.delete(dir);
|
||||
this.onChange(dir);
|
||||
}, DEBOUNCE_MS),
|
||||
);
|
||||
}
|
||||
|
||||
private drop(dir: string): void {
|
||||
this.handles.get(dir)?.close();
|
||||
this.handles.delete(dir);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const handle of this.handles.values()) handle.close();
|
||||
this.handles.clear();
|
||||
for (const pending of this.debounces.values()) clearTimeout(pending);
|
||||
this.debounces.clear();
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -6,6 +6,7 @@ import { PanelContextMenu } from "./context-menu";
|
||||
import { EditManager } from "./edits";
|
||||
import { EditorWindows } from "./editor-windows";
|
||||
import { SessionRegistry } from "./fs/registry";
|
||||
import { LocalDirWatcher } from "./fs/watcher";
|
||||
import { ManagerWindow } from "./manager-window";
|
||||
import { applyApplicationMenu } from "./menu";
|
||||
import { createAppRPC } from "./rpc-handlers";
|
||||
@@ -45,6 +46,7 @@ const store = new ConnectionStore();
|
||||
const settings = new SettingsStore();
|
||||
const edits = new EditManager(registry, engine, settings);
|
||||
const contextMenu = new PanelContextMenu();
|
||||
const watcher = new LocalDirWatcher();
|
||||
const events = {
|
||||
sessionClosed: (_info: { sessionId: string; reason: string }) => {},
|
||||
connectionsChanged: () => {},
|
||||
@@ -53,7 +55,9 @@ const events = {
|
||||
openConnectionManager: () => {},
|
||||
showAbout: () => {},
|
||||
};
|
||||
const rpc = createAppRPC({ registry, engine, store, settings, edits, contextMenu, events });
|
||||
const rpc = createAppRPC({ registry, engine, store, settings, edits, contextMenu, watcher, events });
|
||||
// late-bound like events: a watched local dir changed -> tell the main webview
|
||||
watcher.onChange = (dir) => rpc.send.pathRefresh({ sessionId: "local", dir });
|
||||
const url = await getMainViewUrl();
|
||||
|
||||
// Loaded before any window exists: the persisted theme is baked into every
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { PanelContextMenu } from "./context-menu";
|
||||
import { readFileText, writeFileText, type EditManager } from "./edits";
|
||||
import { listLocalDrives } from "./fs/local";
|
||||
import type { SessionRegistry } from "./fs/registry";
|
||||
import type { LocalDirWatcher } from "./fs/watcher";
|
||||
import { SftpProvider } from "./fs/sftp";
|
||||
import type { ConnectionStore } from "./store/connections";
|
||||
import type { SettingsStore } from "./store/settings";
|
||||
@@ -24,6 +25,7 @@ export interface AppDeps {
|
||||
settings: SettingsStore;
|
||||
edits: EditManager;
|
||||
contextMenu: PanelContextMenu;
|
||||
watcher: LocalDirWatcher;
|
||||
/** Assigned after the rpc object exists (it needs rpc.send). */
|
||||
events: {
|
||||
sessionClosed: (info: { sessionId: string; reason: string }) => void;
|
||||
@@ -121,6 +123,7 @@ export function createAppRPC({
|
||||
settings,
|
||||
edits,
|
||||
contextMenu,
|
||||
watcher,
|
||||
events,
|
||||
}: AppDeps) {
|
||||
return BrowserView.defineRPC<AppRPC>({
|
||||
@@ -160,6 +163,12 @@ export function createAppRPC({
|
||||
return { path: dir, entries: await fs.list(dir) };
|
||||
}),
|
||||
|
||||
setWatchedDirs: ({ dirs }) =>
|
||||
wrap(async () => {
|
||||
watcher.setWatched(dirs);
|
||||
return {};
|
||||
}),
|
||||
|
||||
mkdir: ({ sessionId, path }) =>
|
||||
wrap(async () => {
|
||||
await registry.get(sessionId).mkdir(path);
|
||||
|
||||
@@ -44,6 +44,9 @@ export type AppRPC = {
|
||||
params: { sessionId: string; path: string | null }; // null -> home dir
|
||||
response: Result<{ path: string; entries: FileEntry[] }>;
|
||||
};
|
||||
// Local dirs to fs.watch for auto-refresh (the active tab's local panels);
|
||||
// a change pushes pathRefresh. Remote panels are polled UI-side instead.
|
||||
setWatchedDirs: { params: { dirs: string[] }; response: Result };
|
||||
mkdir: { params: { sessionId: string; path: string }; response: Result };
|
||||
createFile: { params: { sessionId: string; path: string }; response: Result };
|
||||
rename: {
|
||||
|
||||
@@ -95,6 +95,10 @@ export interface AppSettings {
|
||||
editor: "builtin" | "custom";
|
||||
/** Full path to the external editor .app; empty = OS default application. */
|
||||
editorApp: string;
|
||||
// Auto-refresh the active tab's panels (local watched, remote polled). Always
|
||||
// on for now; uncomment (+ the settings.service and tab-bar breadcrumbs, and
|
||||
// gate the poll/watch in panels.service) to expose a toggle later.
|
||||
// autoRefresh: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: AppSettings = {
|
||||
@@ -110,6 +114,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
||||
autoClearCompleted: true,
|
||||
editor: "builtin",
|
||||
editorApp: "",
|
||||
// autoRefresh: true,
|
||||
};
|
||||
|
||||
export function effectiveConcurrency(settings: AppSettings): number {
|
||||
|
||||
Reference in New Issue
Block a user