fix(file-tree): keep aux-panel tree in sync with filesystem changes
Deep directories (beyond the workspace snapshot's depth limit) relied on a lazy-loaded override cache that was never invalidated, so agent-created files, in-app deletes / renames / rollbacks, and external changes inside expanded deep folders stayed invisible until the folder was reopened. Propagate watcher `changed_paths` through the delta envelope and fire a Meta event whenever FS activity doesn't alter the tree/git snapshots, so the frontend can surgically invalidate affected cache entries and re-fetch. Manual refresh (Reload from disk) clears the cache and re-hydrates still-expanded deep dirs through the same path. Replayed deltas after reconnect are forwarded to the same listeners. Also split the combined workspace-state effect into tree / git / status slices so unrelated state transitions (e.g. the 'resyncing' flip during a refresh) no longer rebuild the entire node tree and cause a flash.
This commit is contained in:
@@ -48,6 +48,8 @@ pub struct WorkspaceDeltaEnvelope {
|
|||||||
pub kind: String,
|
pub kind: String,
|
||||||
pub payload: Vec<WorkspaceDelta>,
|
pub payload: Vec<WorkspaceDelta>,
|
||||||
pub requires_resync: bool,
|
pub requires_resync: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub changed_paths: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -58,6 +60,8 @@ pub struct WorkspaceStateEvent {
|
|||||||
pub kind: String,
|
pub kind: String,
|
||||||
pub payload: Vec<WorkspaceDelta>,
|
pub payload: Vec<WorkspaceDelta>,
|
||||||
pub requires_resync: bool,
|
pub requires_resync: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub changed_paths: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -108,6 +112,7 @@ impl WorkspaceStateCore {
|
|||||||
kind: String,
|
kind: String,
|
||||||
payload: Vec<WorkspaceDelta>,
|
payload: Vec<WorkspaceDelta>,
|
||||||
requires_resync: bool,
|
requires_resync: bool,
|
||||||
|
changed_paths: Vec<String>,
|
||||||
) -> WorkspaceStateEvent {
|
) -> WorkspaceStateEvent {
|
||||||
self.seq += 1;
|
self.seq += 1;
|
||||||
|
|
||||||
@@ -120,6 +125,7 @@ impl WorkspaceStateCore {
|
|||||||
kind: kind.clone(),
|
kind: kind.clone(),
|
||||||
payload: payload.clone(),
|
payload: payload.clone(),
|
||||||
requires_resync,
|
requires_resync,
|
||||||
|
changed_paths: changed_paths.clone(),
|
||||||
};
|
};
|
||||||
self.push_recent_event(envelope);
|
self.push_recent_event(envelope);
|
||||||
|
|
||||||
@@ -130,6 +136,7 @@ impl WorkspaceStateCore {
|
|||||||
kind,
|
kind,
|
||||||
payload,
|
payload,
|
||||||
requires_resync,
|
requires_resync,
|
||||||
|
changed_paths,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -713,6 +720,17 @@ async fn flush_watch_batch(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Surface FS activity that doesn't otherwise change tree/git snapshots
|
||||||
|
// (e.g. files added/removed in a directory beyond WORKSPACE_TREE_MAX_DEPTH,
|
||||||
|
// or gitignored / non-git-repo changes). The envelope's `changed_paths`
|
||||||
|
// lets the frontend invalidate its lazy-loaded overrides for deep
|
||||||
|
// directories without waiting for a manual reload.
|
||||||
|
if payload.is_empty() && !changed_paths.is_empty() {
|
||||||
|
payload.push(WorkspaceDelta::Meta {
|
||||||
|
reason: "fs_events".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if payload.is_empty() {
|
if payload.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -731,7 +749,7 @@ async fn flush_watch_batch(
|
|||||||
"meta".to_string()
|
"meta".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
guard.append_event(kind, payload, git_presence_changed)
|
guard.append_event(kind, payload, git_presence_changed, changed_paths)
|
||||||
};
|
};
|
||||||
|
|
||||||
emit_event(emitter, "folder://workspace-state-event", event);
|
emit_event(emitter, "folder://workspace-state-event", event);
|
||||||
@@ -1072,6 +1090,7 @@ mod tests {
|
|||||||
reason: "boot".to_string(),
|
reason: "boot".to_string(),
|
||||||
}],
|
}],
|
||||||
false,
|
false,
|
||||||
|
Vec::new(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let e2 = core.append_event(
|
let e2 = core.append_event(
|
||||||
@@ -1080,6 +1099,7 @@ mod tests {
|
|||||||
reason: "tick".to_string(),
|
reason: "tick".to_string(),
|
||||||
}],
|
}],
|
||||||
false,
|
false,
|
||||||
|
Vec::new(),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(e2.seq > e1.seq);
|
assert!(e2.seq > e1.seq);
|
||||||
@@ -1095,6 +1115,7 @@ mod tests {
|
|||||||
reason: "a".to_string(),
|
reason: "a".to_string(),
|
||||||
}],
|
}],
|
||||||
false,
|
false,
|
||||||
|
Vec::new(),
|
||||||
);
|
);
|
||||||
|
|
||||||
core.append_event(
|
core.append_event(
|
||||||
@@ -1103,6 +1124,7 @@ mod tests {
|
|||||||
reason: "b".to_string(),
|
reason: "b".to_string(),
|
||||||
}],
|
}],
|
||||||
false,
|
false,
|
||||||
|
Vec::new(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let snapshot = core.snapshot(Some(e1.seq));
|
let snapshot = core.snapshot(Some(e1.seq));
|
||||||
@@ -1123,6 +1145,7 @@ mod tests {
|
|||||||
reason: "a".to_string(),
|
reason: "a".to_string(),
|
||||||
}],
|
}],
|
||||||
false,
|
false,
|
||||||
|
Vec::new(),
|
||||||
);
|
);
|
||||||
core.append_event(
|
core.append_event(
|
||||||
"meta".to_string(),
|
"meta".to_string(),
|
||||||
@@ -1130,6 +1153,7 @@ mod tests {
|
|||||||
reason: "b".to_string(),
|
reason: "b".to_string(),
|
||||||
}],
|
}],
|
||||||
false,
|
false,
|
||||||
|
Vec::new(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let snapshot = core.snapshot(Some(0));
|
let snapshot = core.snapshot(Some(0));
|
||||||
|
|||||||
@@ -888,6 +888,11 @@ export function FileTreeTab() {
|
|||||||
new Map()
|
new Map()
|
||||||
)
|
)
|
||||||
const lazyLoadingDirPathsRef = useRef<Set<string>>(new Set())
|
const lazyLoadingDirPathsRef = useRef<Set<string>>(new Set())
|
||||||
|
const loadDirectoryChildrenRef = useRef<
|
||||||
|
((dirPath: string) => Promise<void>) | null
|
||||||
|
>(null)
|
||||||
|
const expandedPathsRef = useRef<Set<string>>(new Set([FILE_TREE_ROOT_PATH]))
|
||||||
|
const workspaceTreeRef = useRef<FileTreeNode[]>([])
|
||||||
const externalConflictSignatureByPathRef = useRef<Map<string, string>>(
|
const externalConflictSignatureByPathRef = useRef<Map<string, string>>(
|
||||||
new Map()
|
new Map()
|
||||||
)
|
)
|
||||||
@@ -967,36 +972,58 @@ export function FileTreeTab() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drop the lazy-load override cache so the fresh snapshot is not
|
||||||
|
// masked by stale children (e.g. after deletes / renames / rollbacks
|
||||||
|
// or files the agent just created). Reading expanded paths via a ref
|
||||||
|
// keeps fetchTree's identity stable across expand/collapse so
|
||||||
|
// downstream memoization is not invalidated on every tree interaction.
|
||||||
|
const pathsToReload = Array.from(expandedPathsRef.current).filter(
|
||||||
|
(path) => path !== FILE_TREE_ROOT_PATH
|
||||||
|
)
|
||||||
|
lazyLoadedChildrenByPathRef.current.clear()
|
||||||
await workspaceState.requestResync("manual_refresh")
|
await workspaceState.requestResync("manual_refresh")
|
||||||
|
// Re-hydrate children for directories beyond WORKSPACE_TREE_MAX_DEPTH
|
||||||
|
// that are still expanded — the backend snapshot does not include them.
|
||||||
|
const loader = loadDirectoryChildrenRef.current
|
||||||
|
if (loader) {
|
||||||
|
for (const path of pathsToReload) {
|
||||||
|
void loader(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[folder?.path, workspaceState]
|
[folder?.path, workspaceState]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Tree updates are the only source that should cause a full setNodes.
|
||||||
|
// applyLazyTreeOverrides rebuilds every directory node object, which forces
|
||||||
|
// React to re-render the entire tree. Keeping this effect narrow avoids
|
||||||
|
// wasted work on health / seq / error / git transitions that don't touch
|
||||||
|
// the tree shape (e.g. the intermediate "resyncing" patch during a refresh).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
workspaceTreeRef.current = workspaceState.tree
|
||||||
setNodes(
|
setNodes(
|
||||||
applyLazyTreeOverrides(
|
applyLazyTreeOverrides(
|
||||||
workspaceState.tree,
|
workspaceState.tree,
|
||||||
lazyLoadedChildrenByPathRef.current
|
lazyLoadedChildrenByPathRef.current
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
}, [folder?.path, workspaceState.tree])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
const nextStatusByPath = new Map<string, string>()
|
const nextStatusByPath = new Map<string, string>()
|
||||||
for (const entry of workspaceState.git) {
|
for (const entry of workspaceState.git) {
|
||||||
nextStatusByPath.set(entry.path, entry.status)
|
nextStatusByPath.set(entry.path, entry.status)
|
||||||
}
|
}
|
||||||
setGitStatusByPath(nextStatusByPath)
|
setGitStatusByPath(nextStatusByPath)
|
||||||
setGitEnabled(true)
|
setGitEnabled(true)
|
||||||
|
}, [workspaceState.git])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
setLoading(
|
setLoading(
|
||||||
workspaceState.health === "resyncing" && workspaceState.seq === 0
|
workspaceState.health === "resyncing" && workspaceState.seq === 0
|
||||||
)
|
)
|
||||||
setError(workspaceState.health === "degraded" ? workspaceState.error : null)
|
setError(workspaceState.health === "degraded" ? workspaceState.error : null)
|
||||||
}, [
|
}, [workspaceState.error, workspaceState.health, workspaceState.seq])
|
||||||
folder?.path,
|
|
||||||
workspaceState.error,
|
|
||||||
workspaceState.git,
|
|
||||||
workspaceState.health,
|
|
||||||
workspaceState.seq,
|
|
||||||
workspaceState.tree,
|
|
||||||
])
|
|
||||||
|
|
||||||
const loadDirectoryChildren = useCallback(
|
const loadDirectoryChildren = useCallback(
|
||||||
async (dirPath: string) => {
|
async (dirPath: string) => {
|
||||||
@@ -1007,12 +1034,19 @@ export function FileTreeTab() {
|
|||||||
if (lazyLoadedChildrenByPathRef.current.has(normalizedDirPath)) return
|
if (lazyLoadedChildrenByPathRef.current.has(normalizedDirPath)) return
|
||||||
if (lazyLoadingDirPathsRef.current.has(normalizedDirPath)) return
|
if (lazyLoadingDirPathsRef.current.has(normalizedDirPath)) return
|
||||||
|
|
||||||
const existingChildren = findDirectoryChildren(nodes, normalizedDirPath)
|
// Check the backend tree (source of truth), not the rendered `nodes`.
|
||||||
if (existingChildren && existingChildren.length > 0) {
|
// `nodes` carries stale lazy-cache overrides that don't invalidate
|
||||||
lazyLoadedChildrenByPathRef.current.set(
|
// until a tree_replace delta arrives — but for directories beyond
|
||||||
normalizedDirPath,
|
// WORKSPACE_TREE_MAX_DEPTH the backend never emits tree_replace for
|
||||||
existingChildren
|
// changes inside them (their children are not in tree_snapshot, so
|
||||||
|
// the refreshed tree compares equal to the old one). Checking
|
||||||
|
// `nodes` would cause fetchTree's forced reload to short-circuit on
|
||||||
|
// the stale override and miss deletions / creations in deep dirs.
|
||||||
|
const existingChildren = findDirectoryChildren(
|
||||||
|
workspaceTreeRef.current,
|
||||||
|
normalizedDirPath
|
||||||
)
|
)
|
||||||
|
if (existingChildren && existingChildren.length > 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1033,9 +1067,90 @@ export function FileTreeTab() {
|
|||||||
lazyLoadingDirPathsRef.current.delete(normalizedDirPath)
|
lazyLoadingDirPathsRef.current.delete(normalizedDirPath)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[folder?.path, nodes]
|
[folder?.path]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadDirectoryChildrenRef.current = loadDirectoryChildren
|
||||||
|
}, [loadDirectoryChildren])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
expandedPathsRef.current = expandedPaths
|
||||||
|
}, [expandedPaths])
|
||||||
|
|
||||||
|
// Subscribe to workspace envelopes to invalidate lazy-loaded overrides for
|
||||||
|
// directories beyond WORKSPACE_TREE_MAX_DEPTH. Those directories are never
|
||||||
|
// reflected in the backend's depth-2 tree_snapshot, so changes inside them
|
||||||
|
// don't emit a tree_replace delta — the frontend has to target invalidation
|
||||||
|
// by matching each `changed_paths` entry against its cached ancestors.
|
||||||
|
// The backend already debounces raw FS events (1s / 3s max), so we only
|
||||||
|
// need a microtask hop here to merge paths that hit the same cached
|
||||||
|
// ancestor within one envelope (or any synchronous burst of envelopes).
|
||||||
|
const subscribeWorkspaceEnvelopes = workspaceState.subscribeEnvelopes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!subscribeWorkspaceEnvelopes) return
|
||||||
|
|
||||||
|
const pendingPaths = new Set<string>()
|
||||||
|
let flushScheduled = false
|
||||||
|
let disposed = false
|
||||||
|
|
||||||
|
const flushPending = () => {
|
||||||
|
flushScheduled = false
|
||||||
|
if (disposed || pendingPaths.size === 0) return
|
||||||
|
const paths = Array.from(pendingPaths)
|
||||||
|
pendingPaths.clear()
|
||||||
|
|
||||||
|
const loader = loadDirectoryChildrenRef.current
|
||||||
|
const cache = lazyLoadedChildrenByPathRef.current
|
||||||
|
const invalidated = new Set<string>()
|
||||||
|
|
||||||
|
for (const changed of paths) {
|
||||||
|
const normalized = normalizeComparePath(changed)
|
||||||
|
if (!normalized) continue
|
||||||
|
// Walk from the changed path up to the root, invalidating the
|
||||||
|
// nearest cached ancestor (or the path itself if a directory of
|
||||||
|
// that name is in the cache). Checking the path itself covers
|
||||||
|
// FS events that report the directory path directly.
|
||||||
|
let cursor = normalized
|
||||||
|
while (cursor.length > 0) {
|
||||||
|
if (cache.has(cursor)) {
|
||||||
|
invalidated.add(cursor)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const slash = cursor.lastIndexOf("/")
|
||||||
|
const parent = slash === -1 ? "" : cursor.slice(0, slash)
|
||||||
|
if (parent.length === 0) break
|
||||||
|
cursor = parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invalidated.size === 0) return
|
||||||
|
for (const path of invalidated) {
|
||||||
|
cache.delete(path)
|
||||||
|
}
|
||||||
|
if (!loader) return
|
||||||
|
for (const path of invalidated) {
|
||||||
|
void loader(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsubscribe = subscribeWorkspaceEnvelopes(({ changed_paths }) => {
|
||||||
|
if (!changed_paths || changed_paths.length === 0) return
|
||||||
|
for (const path of changed_paths) {
|
||||||
|
pendingPaths.add(path)
|
||||||
|
}
|
||||||
|
if (flushScheduled) return
|
||||||
|
flushScheduled = true
|
||||||
|
queueMicrotask(flushPending)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
disposed = true
|
||||||
|
unsubscribe()
|
||||||
|
pendingPaths.clear()
|
||||||
|
}
|
||||||
|
}, [subscribeWorkspaceEnvelopes])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const previousExpanded = previousExpandedPathsRef.current
|
const previousExpanded = previousExpandedPathsRef.current
|
||||||
for (const path of expandedPaths) {
|
for (const path of expandedPaths) {
|
||||||
|
|||||||
@@ -30,9 +30,16 @@ export interface WorkspaceStateView {
|
|||||||
isGitRepo: boolean
|
isGitRepo: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WorkspaceEnvelopeListener = (envelope: {
|
||||||
|
seq: number
|
||||||
|
kind: string
|
||||||
|
changed_paths: string[]
|
||||||
|
}) => void
|
||||||
|
|
||||||
export interface WorkspaceStateResult extends WorkspaceStateView {
|
export interface WorkspaceStateResult extends WorkspaceStateView {
|
||||||
requestResync: (reason?: string) => Promise<void>
|
requestResync: (reason?: string) => Promise<void>
|
||||||
restart: () => Promise<void>
|
restart: () => Promise<void>
|
||||||
|
subscribeEnvelopes: (listener: WorkspaceEnvelopeListener) => () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const WORKSPACE_PROTOCOL_VERSION = 1
|
const WORKSPACE_PROTOCOL_VERSION = 1
|
||||||
@@ -140,6 +147,7 @@ class WorkspaceStateStore {
|
|||||||
private readonly rootPath: string
|
private readonly rootPath: string
|
||||||
private readonly normalizedRootPath: string
|
private readonly normalizedRootPath: string
|
||||||
private listeners = new Set<() => void>()
|
private listeners = new Set<() => void>()
|
||||||
|
private envelopeListeners = new Set<WorkspaceEnvelopeListener>()
|
||||||
private state: WorkspaceStateView
|
private state: WorkspaceStateView
|
||||||
private refCount = 0
|
private refCount = 0
|
||||||
private started = false
|
private started = false
|
||||||
@@ -171,6 +179,13 @@ class WorkspaceStateStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subscribeEnvelopes = (listener: WorkspaceEnvelopeListener): (() => void) => {
|
||||||
|
this.envelopeListeners.add(listener)
|
||||||
|
return () => {
|
||||||
|
this.envelopeListeners.delete(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
acquire = () => {
|
acquire = () => {
|
||||||
this.cancelPendingShutdown()
|
this.cancelPendingShutdown()
|
||||||
this.cancelEviction()
|
this.cancelEviction()
|
||||||
@@ -212,6 +227,16 @@ class WorkspaceStateStore {
|
|||||||
this.patchState((prev) => applySnapshot(prev, snapshot))
|
this.patchState((prev) => applySnapshot(prev, snapshot))
|
||||||
if (snapshot.full) {
|
if (snapshot.full) {
|
||||||
this.hasBaselineSnapshot = true
|
this.hasBaselineSnapshot = true
|
||||||
|
} else {
|
||||||
|
// Forward replayed envelopes so downstream cache-invalidation
|
||||||
|
// hooks catch up on FS activity that happened while disconnected.
|
||||||
|
for (const envelope of snapshot.deltas) {
|
||||||
|
this.notifyEnvelope({
|
||||||
|
seq: envelope.seq,
|
||||||
|
kind: envelope.kind,
|
||||||
|
changed_paths: envelope.changed_paths ?? [],
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.patchState((prev) => ({
|
this.patchState((prev) => ({
|
||||||
@@ -315,6 +340,15 @@ class WorkspaceStateStore {
|
|||||||
)
|
)
|
||||||
if (!this.isLifecycleActive(lifecycleId)) return
|
if (!this.isLifecycleActive(lifecycleId)) return
|
||||||
this.patchState((prev) => applySnapshot(prev, catchUpSnapshot))
|
this.patchState((prev) => applySnapshot(prev, catchUpSnapshot))
|
||||||
|
if (!catchUpSnapshot.full) {
|
||||||
|
for (const envelope of catchUpSnapshot.deltas) {
|
||||||
|
this.notifyEnvelope({
|
||||||
|
seq: envelope.seq,
|
||||||
|
kind: envelope.kind,
|
||||||
|
changed_paths: envelope.changed_paths ?? [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.patchState((prev) => ({
|
this.patchState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -418,6 +452,26 @@ class WorkspaceStateStore {
|
|||||||
health: "healthy",
|
health: "healthy",
|
||||||
error: null,
|
error: null,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
this.notifyEnvelope({
|
||||||
|
seq: event.seq,
|
||||||
|
kind: event.kind,
|
||||||
|
changed_paths: event.changed_paths ?? [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private notifyEnvelope = (envelope: {
|
||||||
|
seq: number
|
||||||
|
kind: string
|
||||||
|
changed_paths: string[]
|
||||||
|
}) => {
|
||||||
|
for (const listener of this.envelopeListeners) {
|
||||||
|
try {
|
||||||
|
listener(envelope)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[workspace-state] envelope listener failed", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private patchState = (
|
private patchState = (
|
||||||
@@ -509,11 +563,20 @@ export function useWorkspaceStateStore(
|
|||||||
await store.restart()
|
await store.restart()
|
||||||
}, [store])
|
}, [store])
|
||||||
|
|
||||||
|
const subscribeEnvelopes = useCallback(
|
||||||
|
(listener: WorkspaceEnvelopeListener) => {
|
||||||
|
if (!store) return () => {}
|
||||||
|
return store.subscribeEnvelopes(listener)
|
||||||
|
},
|
||||||
|
[store]
|
||||||
|
)
|
||||||
|
|
||||||
if (!rootPath) {
|
if (!rootPath) {
|
||||||
return {
|
return {
|
||||||
...EMPTY_STATE,
|
...EMPTY_STATE,
|
||||||
requestResync,
|
requestResync,
|
||||||
restart,
|
restart,
|
||||||
|
subscribeEnvelopes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,5 +584,6 @@ export function useWorkspaceStateStore(
|
|||||||
...snapshot,
|
...snapshot,
|
||||||
requestResync,
|
requestResync,
|
||||||
restart,
|
restart,
|
||||||
|
subscribeEnvelopes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -879,6 +879,7 @@ export interface WorkspaceDeltaEnvelope {
|
|||||||
kind: "fs_delta" | "git_delta" | "meta" | "resync_hint" | string
|
kind: "fs_delta" | "git_delta" | "meta" | "resync_hint" | string
|
||||||
payload: WorkspaceDelta[]
|
payload: WorkspaceDelta[]
|
||||||
requires_resync: boolean
|
requires_resync: boolean
|
||||||
|
changed_paths?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspaceStateEvent {
|
export interface WorkspaceStateEvent {
|
||||||
@@ -888,6 +889,7 @@ export interface WorkspaceStateEvent {
|
|||||||
kind: "fs_delta" | "git_delta" | "meta" | "resync_hint" | string
|
kind: "fs_delta" | "git_delta" | "meta" | "resync_hint" | string
|
||||||
payload: WorkspaceDelta[]
|
payload: WorkspaceDelta[]
|
||||||
requires_resync: boolean
|
requires_resync: boolean
|
||||||
|
changed_paths?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspaceSnapshotResponse {
|
export interface WorkspaceSnapshotResponse {
|
||||||
|
|||||||
Reference in New Issue
Block a user