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:
+15
-1
@@ -114,6 +114,12 @@ would float over editor windows).
|
|||||||
callback. mtime seconds→ms; mode masked `& 0o7777`.
|
callback. mtime seconds→ms; mode masked `& 0o7777`.
|
||||||
- `fs/registry.ts` — sessionId → provider map; `"local"` is built-in;
|
- `fs/registry.ts` — sessionId → provider map; `"local"` is built-in;
|
||||||
`add()` mints `sftp-<n>`; `drop()` (session died) vs `remove()` (clean).
|
`add()` mints `sftp-<n>`; `drop()` (session died) vs `remove()` (clean).
|
||||||
|
- `fs/watcher.ts` — `LocalDirWatcher`: `fs.watch` handles keyed by dir,
|
||||||
|
debounced ~150 ms (watch fires bursts). Drives local auto-refresh: the main
|
||||||
|
webview sends `setWatchedDirs` (the active tab's local panel cwds); a change
|
||||||
|
fires `onChange` → `rpc.send.pathRefresh({sessionId:"local", dir})`.
|
||||||
|
`onChange` is late-bound in index.ts (the `events` pattern). SFTP has no
|
||||||
|
watch, so remote panels are polled UI-side instead (see panels.service).
|
||||||
- `transfers/engine.ts` — FIFO job queue. `Semaphore` with live resize
|
- `transfers/engine.ts` — FIFO job queue. `Semaphore` with live resize
|
||||||
(`wake()` reserves slots synchronously — an async waiter increment was a
|
(`wake()` reserves slots synchronously — an async waiter increment was a
|
||||||
race). Per-job worker pool (`MAX_WORKERS_PER_JOB=8`) draws from the shared
|
race). Per-job worker pool (`MAX_WORKERS_PER_JOB=8`) draws from the shared
|
||||||
@@ -172,7 +178,15 @@ would float over editor windows).
|
|||||||
auto-load `localDir` into an untouched local panel). `connectToken`
|
auto-load `localDir` into an untouched local panel). `connectToken`
|
||||||
supersedes stale connects. `Workspace` = tab = left/right pair + activeSide.
|
supersedes stale connects. `Workspace` = tab = left/right pair + activeSide.
|
||||||
Remote goes RIGHT on connect/open-stack (commander convention); per-panel
|
Remote goes RIGHT on connect/open-stack (commander convention); per-panel
|
||||||
dropdown connects target their own panel.
|
dropdown connects target their own panel. **Auto-refresh** (always on):
|
||||||
|
`refresh({silent})` re-lists without the loading/busy flags and touches no
|
||||||
|
DOM (scroll/cursor/selection/filter preserved), snapshotting sessionId+cwd
|
||||||
|
to discard results if the user navigated mid-flight. Only the active tab is
|
||||||
|
live: its local panels are fs.watched (backend `setWatchedDirs` effect +
|
||||||
|
`pathRefresh` push → silent refresh), its remote panels polled every 3s
|
||||||
|
(`REMOTE_POLL_MS`); switching tabs silently refreshes the newly-active pair.
|
||||||
|
A future on/off setting is stubbed in commented breadcrumbs (types.ts,
|
||||||
|
settings.service, tab-bar).
|
||||||
- `transfers.service.ts` — jobMap from `transferUpdate` callbacks; EMA speed
|
- `transfers.service.ts` — jobMap from `transferUpdate` callbacks; EMA speed
|
||||||
(`SPEED_SMOOTHING=0.7`); `copyToOtherPanel` guards same-fs+same-dir before
|
(`SPEED_SMOOTHING=0.7`); `copyToOtherPanel` guards same-fs+same-dir before
|
||||||
enqueue and runs the collision flow (same-path auto-skip, type-mismatch
|
enqueue and runs the collision flow (same-path auto-skip, type-mismatch
|
||||||
|
|||||||
@@ -85,6 +85,12 @@ touching the adjacent code.
|
|||||||
don't shrink the 400 ms debounce.
|
don't shrink the 400 ms debounce.
|
||||||
- `panel.touched` gates localDir auto-navigation; `connectToken` guards
|
- `panel.touched` gates localDir auto-navigation; `connectToken` guards
|
||||||
against a slow connect resolving into a panel the user already reused.
|
against a slow connect resolving into a panel the user already reused.
|
||||||
|
- **Silent auto-refresh must snapshot sessionId+cwd and discard a mismatched
|
||||||
|
result.** A normal `refresh()` is masked by `busy` (input blocked), but the
|
||||||
|
silent variant (poll/watch) runs while the user can navigate — without the
|
||||||
|
guard, a poll that started in dir A resolves and stamps A's listing over the
|
||||||
|
dir B the user just opened. It also never calls `scrollCursorIntoView`
|
||||||
|
(scroll must stay put) and returns early if `busy()` or already refreshing.
|
||||||
|
|
||||||
## Build / tooling
|
## Build / tooling
|
||||||
|
|
||||||
|
|||||||
@@ -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 { EditManager } from "./edits";
|
||||||
import { EditorWindows } from "./editor-windows";
|
import { EditorWindows } from "./editor-windows";
|
||||||
import { SessionRegistry } from "./fs/registry";
|
import { SessionRegistry } from "./fs/registry";
|
||||||
|
import { LocalDirWatcher } from "./fs/watcher";
|
||||||
import { ManagerWindow } from "./manager-window";
|
import { ManagerWindow } from "./manager-window";
|
||||||
import { applyApplicationMenu } from "./menu";
|
import { applyApplicationMenu } from "./menu";
|
||||||
import { createAppRPC } from "./rpc-handlers";
|
import { createAppRPC } from "./rpc-handlers";
|
||||||
@@ -45,6 +46,7 @@ const store = new ConnectionStore();
|
|||||||
const settings = new SettingsStore();
|
const settings = new SettingsStore();
|
||||||
const edits = new EditManager(registry, engine, settings);
|
const edits = new EditManager(registry, engine, settings);
|
||||||
const contextMenu = new PanelContextMenu();
|
const contextMenu = new PanelContextMenu();
|
||||||
|
const watcher = new LocalDirWatcher();
|
||||||
const events = {
|
const events = {
|
||||||
sessionClosed: (_info: { sessionId: string; reason: string }) => {},
|
sessionClosed: (_info: { sessionId: string; reason: string }) => {},
|
||||||
connectionsChanged: () => {},
|
connectionsChanged: () => {},
|
||||||
@@ -53,7 +55,9 @@ const events = {
|
|||||||
openConnectionManager: () => {},
|
openConnectionManager: () => {},
|
||||||
showAbout: () => {},
|
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();
|
const url = await getMainViewUrl();
|
||||||
|
|
||||||
// Loaded before any window exists: the persisted theme is baked into every
|
// 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 { readFileText, writeFileText, type EditManager } from "./edits";
|
||||||
import { listLocalDrives } from "./fs/local";
|
import { listLocalDrives } from "./fs/local";
|
||||||
import type { SessionRegistry } from "./fs/registry";
|
import type { SessionRegistry } from "./fs/registry";
|
||||||
|
import type { LocalDirWatcher } from "./fs/watcher";
|
||||||
import { SftpProvider } from "./fs/sftp";
|
import { SftpProvider } from "./fs/sftp";
|
||||||
import type { ConnectionStore } from "./store/connections";
|
import type { ConnectionStore } from "./store/connections";
|
||||||
import type { SettingsStore } from "./store/settings";
|
import type { SettingsStore } from "./store/settings";
|
||||||
@@ -24,6 +25,7 @@ export interface AppDeps {
|
|||||||
settings: SettingsStore;
|
settings: SettingsStore;
|
||||||
edits: EditManager;
|
edits: EditManager;
|
||||||
contextMenu: PanelContextMenu;
|
contextMenu: PanelContextMenu;
|
||||||
|
watcher: LocalDirWatcher;
|
||||||
/** Assigned after the rpc object exists (it needs rpc.send). */
|
/** Assigned after the rpc object exists (it needs rpc.send). */
|
||||||
events: {
|
events: {
|
||||||
sessionClosed: (info: { sessionId: string; reason: string }) => void;
|
sessionClosed: (info: { sessionId: string; reason: string }) => void;
|
||||||
@@ -121,6 +123,7 @@ export function createAppRPC({
|
|||||||
settings,
|
settings,
|
||||||
edits,
|
edits,
|
||||||
contextMenu,
|
contextMenu,
|
||||||
|
watcher,
|
||||||
events,
|
events,
|
||||||
}: AppDeps) {
|
}: AppDeps) {
|
||||||
return BrowserView.defineRPC<AppRPC>({
|
return BrowserView.defineRPC<AppRPC>({
|
||||||
@@ -160,6 +163,12 @@ export function createAppRPC({
|
|||||||
return { path: dir, entries: await fs.list(dir) };
|
return { path: dir, entries: await fs.list(dir) };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
setWatchedDirs: ({ dirs }) =>
|
||||||
|
wrap(async () => {
|
||||||
|
watcher.setWatched(dirs);
|
||||||
|
return {};
|
||||||
|
}),
|
||||||
|
|
||||||
mkdir: ({ sessionId, path }) =>
|
mkdir: ({ sessionId, path }) =>
|
||||||
wrap(async () => {
|
wrap(async () => {
|
||||||
await registry.get(sessionId).mkdir(path);
|
await registry.get(sessionId).mkdir(path);
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ export type AppRPC = {
|
|||||||
params: { sessionId: string; path: string | null }; // null -> home dir
|
params: { sessionId: string; path: string | null }; // null -> home dir
|
||||||
response: Result<{ path: string; entries: FileEntry[] }>;
|
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 };
|
mkdir: { params: { sessionId: string; path: string }; response: Result };
|
||||||
createFile: { params: { sessionId: string; path: string }; response: Result };
|
createFile: { params: { sessionId: string; path: string }; response: Result };
|
||||||
rename: {
|
rename: {
|
||||||
|
|||||||
@@ -95,6 +95,10 @@ export interface AppSettings {
|
|||||||
editor: "builtin" | "custom";
|
editor: "builtin" | "custom";
|
||||||
/** Full path to the external editor .app; empty = OS default application. */
|
/** Full path to the external editor .app; empty = OS default application. */
|
||||||
editorApp: string;
|
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 = {
|
export const DEFAULT_SETTINGS: AppSettings = {
|
||||||
@@ -110,6 +114,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
|||||||
autoClearCompleted: true,
|
autoClearCompleted: true,
|
||||||
editor: "builtin",
|
editor: "builtin",
|
||||||
editorApp: "",
|
editorApp: "",
|
||||||
|
// autoRefresh: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function effectiveConcurrency(settings: AppSettings): number {
|
export function effectiveConcurrency(settings: AppSettings): number {
|
||||||
|
|||||||
@@ -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 type { ColumnKey, ConnectionConfig, FileEntry, SavedConnection } from '@shared/types';
|
||||||
import { fileKind } from './file-icons';
|
import { fileKind } from './file-icons';
|
||||||
import { FocusService } from './focus.service';
|
import { FocusService } from './focus.service';
|
||||||
@@ -108,19 +108,34 @@ export class PanelState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reload cwd, keeping cursor position and selection by name. */
|
private refreshing = false;
|
||||||
async refresh(): Promise<void> {
|
|
||||||
|
/**
|
||||||
|
* 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 keepCursorName = this.sorted()[this.cursor()]?.name;
|
||||||
const keepSelected = this.selected();
|
const keepSelected = this.selected();
|
||||||
this.loading.set(true);
|
if (!silent) this.loading.set(true);
|
||||||
const slowTimer = setTimeout(() => this.slowLoad.set(true), 300);
|
const slowTimer = silent ? null : setTimeout(() => this.slowLoad.set(true), 300);
|
||||||
try {
|
try {
|
||||||
const res = await this.deps.rpc.request.listDir({
|
const res = await this.deps.rpc.request.listDir({
|
||||||
sessionId: this.sessionId(),
|
sessionId: forSession,
|
||||||
path: this.cwd(),
|
path: forDir,
|
||||||
});
|
});
|
||||||
|
if (this.sessionId() !== forSession || this.cwd() !== forDir) return;
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
this.deps.toast.error(res.error);
|
if (!silent) this.deps.toast.error(res.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.cwd.set(res.path);
|
this.cwd.set(res.path);
|
||||||
@@ -130,9 +145,12 @@ export class PanelState {
|
|||||||
const idx = this.sorted().findIndex((e) => e.name === keepCursorName);
|
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)));
|
this.cursor.set(idx >= 0 ? idx : Math.min(this.cursor(), Math.max(0, this.sorted().length - 1)));
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(slowTimer);
|
if (slowTimer) clearTimeout(slowTimer);
|
||||||
this.slowLoad.set(false);
|
if (!silent) {
|
||||||
this.loading.set(false);
|
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 }) => {
|
this.rpc.onPathRefresh(({ sessionId, dir }) => {
|
||||||
for (const panel of this.allPanels()) {
|
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 ----
|
// ---- panels of the active workspace ----
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ export class SettingsService {
|
|||||||
readonly autoClearCompleted = signal(DEFAULT_SETTINGS.autoClearCompleted);
|
readonly autoClearCompleted = signal(DEFAULT_SETTINGS.autoClearCompleted);
|
||||||
readonly editor = signal<AppSettings['editor']>(DEFAULT_SETTINGS.editor);
|
readonly editor = signal<AppSettings['editor']>(DEFAULT_SETTINGS.editor);
|
||||||
readonly editorApp = signal(DEFAULT_SETTINGS.editorApp);
|
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);
|
private readonly rpc = inject(RpcService);
|
||||||
|
|
||||||
|
|||||||
@@ -342,6 +342,23 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- about footer -->
|
||||||
<button
|
<button
|
||||||
(click)="showAbout()"
|
(click)="showAbout()"
|
||||||
|
|||||||
Reference in New Issue
Block a user