Skip to content

Commit f374015

Browse files
committed
File index: Expose size-pending status via MCP
- Add `recursive_size_pending` to `PaneFileEntry`; render it as a `[size-pending]` marker on directory rows in `cmdr://state` so agents can observe the "size updating" hourglass without DOM access. - Mirror the flag from the FE: `FilePane` includes `recursiveSizePending` in the synced pane entries, and `refreshIndexSizes` now re-syncs MCP state so the marker updates live during an index storm (not just on cursor/nav changes). - Fill the new field in the network-browser pane entries and MCP test fixtures.
1 parent 0afc10b commit f374015

9 files changed

Lines changed: 71 additions & 4 deletions

File tree

apps/desktop/src-tauri/src/mcp/pane_state.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ pub struct PaneFileEntry {
2828
pub size: Option<u64>,
2929
pub recursive_size: Option<u64>,
3030
pub modified: Option<String>,
31+
/// `Some(true)` while the indexer still has unprocessed writes affecting
32+
/// this directory or a descendant, so its recursive size is mid-update.
33+
/// Surfaced in `cmdr://state` as a `[size-pending]` marker so agents can
34+
/// observe the "size updating" hourglass without DOM access. `None`/`false`
35+
/// once the writer drains. Only meaningful for directories.
36+
pub recursive_size_pending: Option<bool>,
3137
}
3238

3339
/// State of a single pane.
@@ -239,6 +245,7 @@ mod tests {
239245
size: Some(100),
240246
recursive_size: None,
241247
modified: None,
248+
recursive_size_pending: None,
242249
}],
243250
cursor_index: 0,
244251
view_mode: "brief".to_string(),

apps/desktop/src-tauri/src/mcp/resources.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,11 @@ fn format_file_compact(
216216
if is_selected {
217217
parts.push("[sel]".to_string());
218218
}
219+
// The recursive size is mid-update (indexer still draining writes for this
220+
// dir or a descendant). Mirrors the per-row "size updating" hourglass.
221+
if file.recursive_size_pending == Some(true) {
222+
parts.push("[size-pending]".to_string());
223+
}
219224

220225
parts.join(" ")
221226
}
@@ -966,6 +971,7 @@ mod tests {
966971
size: Some(1024),
967972
recursive_size: None,
968973
modified: Some("2024-01-15".to_string()),
974+
recursive_size_pending: None,
969975
};
970976

971977
// Without details
@@ -992,6 +998,7 @@ mod tests {
992998
size: None,
993999
recursive_size: None,
9941000
modified: None,
1001+
recursive_size_pending: None,
9951002
};
9961003
let formatted = format_file_compact(&dir, 1, false, false, false);
9971004
assert_eq!(formatted, "i:1 d docs");
@@ -1004,9 +1011,27 @@ mod tests {
10041011
size: None,
10051012
recursive_size: Some(169),
10061013
modified: Some("2026-03-19T17:33:53.000Z".to_string()),
1014+
recursive_size_pending: None,
10071015
};
10081016
let formatted = format_file_compact(&dir_with_size, 5, false, false, true);
10091017
assert_eq!(formatted, "i:5 d src 169 B 2026-03-19T17:33:53.000Z");
1018+
1019+
// Directory whose recursive size is mid-update gets a [size-pending] marker
1020+
// (the "size updating" hourglass, observable without DOM access).
1021+
let pending_dir = PaneFileEntry {
1022+
name: "target".to_string(),
1023+
path: "/tmp/target".to_string(),
1024+
is_directory: true,
1025+
size: None,
1026+
recursive_size: Some(4096),
1027+
modified: None,
1028+
recursive_size_pending: Some(true),
1029+
};
1030+
let formatted = format_file_compact(&pending_dir, 2, false, false, true);
1031+
assert_eq!(formatted, "i:2 d target 4 KB [size-pending]");
1032+
// The marker shows even without details (it's a status, not a detail).
1033+
let formatted = format_file_compact(&pending_dir, 2, false, false, false);
1034+
assert_eq!(formatted, "i:2 d target [size-pending]");
10101035
}
10111036

10121037
#[test]
@@ -1023,6 +1048,7 @@ mod tests {
10231048
size: Some(100),
10241049
recursive_size: None,
10251050
modified: Some("2024-01-15".to_string()),
1051+
recursive_size_pending: None,
10261052
},
10271053
PaneFileEntry {
10281054
name: "folder".to_string(),
@@ -1031,6 +1057,7 @@ mod tests {
10311057
size: None,
10321058
recursive_size: None,
10331059
modified: None,
1060+
recursive_size_pending: None,
10341061
},
10351062
],
10361063
cursor_index: 0,

apps/desktop/src-tauri/src/mcp/tests/ack_system_tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ fn generation_gate_flips_true_after_pane_push() {
103103
size: None,
104104
recursive_size: None,
105105
modified: None,
106+
recursive_size_pending: None,
106107
}],
107108
..Default::default()
108109
});

apps/desktop/src-tauri/src/mcp/tests/pane_state_tests.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ fn test_pane_state_store_update_left() {
2727
size: Some(1024),
2828
recursive_size: None,
2929
modified: Some("2024-01-01T00:00:00Z".to_string()),
30+
recursive_size_pending: None,
3031
}],
3132
cursor_index: 0,
3233
view_mode: "brief".to_string(),
@@ -89,6 +90,7 @@ fn test_pane_state_cursor_index_bounds() {
8990
size: None,
9091
recursive_size: None,
9192
modified: None,
93+
recursive_size_pending: None,
9294
}],
9395
cursor_index: 999, // Out of bounds
9496
view_mode: "brief".to_string(),
@@ -123,6 +125,7 @@ fn test_file_entry_serialization() {
123125
size: Some(42),
124126
recursive_size: None,
125127
modified: Some("2024-01-01T00:00:00Z".to_string()),
128+
recursive_size_pending: None,
126129
};
127130

128131
let json = serde_json::to_value(&entry).unwrap();
@@ -140,6 +143,7 @@ fn test_file_entry_optional_fields_serialize_as_null() {
140143
size: None,
141144
recursive_size: None,
142145
modified: None,
146+
recursive_size_pending: None,
143147
};
144148

145149
let json = serde_json::to_value(&entry).unwrap();
@@ -174,6 +178,7 @@ fn test_unicode_in_file_entries() {
174178
size: Some(100),
175179
recursive_size: None,
176180
modified: None,
181+
recursive_size_pending: None,
177182
};
178183

179184
let json = serde_json::to_value(&entry).unwrap();
@@ -191,6 +196,7 @@ fn test_special_chars_in_file_paths() {
191196
size: None,
192197
recursive_size: None,
193198
modified: None,
199+
recursive_size_pending: None,
194200
},
195201
PaneFileEntry {
196202
name: "file'with'quotes.txt".to_string(),
@@ -199,6 +205,7 @@ fn test_special_chars_in_file_paths() {
199205
size: None,
200206
recursive_size: None,
201207
modified: None,
208+
recursive_size_pending: None,
202209
},
203210
PaneFileEntry {
204211
name: "file\"doublequotes\".txt".to_string(),
@@ -207,6 +214,7 @@ fn test_special_chars_in_file_paths() {
207214
size: None,
208215
recursive_size: None,
209216
modified: None,
217+
recursive_size_pending: None,
210218
},
211219
];
212220

@@ -252,6 +260,7 @@ fn test_large_file_count() {
252260
size: Some(i as u64 * 100),
253261
recursive_size: None,
254262
modified: None,
263+
recursive_size_pending: None,
255264
})
256265
.collect();
257266

apps/desktop/src-tauri/src/mcp/tests/security_tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ fn test_null_bytes_in_paths() {
130130
size: None,
131131
recursive_size: None,
132132
modified: None,
133+
recursive_size_pending: None,
133134
};
134135

135136
// Should serialize without panic

apps/desktop/src/lib/file-explorer/network/NetworkBrowser.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@
133133
size: null,
134134
recursiveSize: null,
135135
modified: null,
136-
}
136+
recursiveSizePending: null, }
137137
})
138138
139139
// Add the "Connect to server..." pseudo-row for MCP visibility
@@ -144,7 +144,7 @@
144144
size: null,
145145
recursiveSize: null,
146146
modified: null,
147-
})
147+
recursiveSizePending: null, })
148148
149149
const state: PaneState = {
150150
path: 'smb://',

apps/desktop/src/lib/file-explorer/network/ShareBrowser.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@
129129
size: null,
130130
recursiveSize: null,
131131
modified: null,
132-
}))
132+
recursiveSizePending: null, }))
133133
134134
const state: PaneState = {
135135
path: `smb://${host.ipAddress ?? host.name}/`,

apps/desktop/src/lib/file-explorer/pane/FilePane.svelte

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,11 @@
985985
if (listingId) {
986986
void refreshListingIndexSizes(listingId).then(() => fetchListingStats())
987987
}
988+
// Mirror the refreshed sizes (and the `recursiveSizePending` hourglass flag)
989+
// into the MCP pane state so agents see `[size-pending]` update live during
990+
// an index storm, not just on cursor/nav changes. Debounced (300ms), so a
991+
// burst of index-dir-updated refreshes coalesces into one sync.
992+
debouncedSyncMcp.call()
988993
}
989994
990995
export function getSwapState(): SwapState {
@@ -1163,7 +1168,15 @@
11631168
// Include ".." entry if it's in the visible range
11641169
if (hasParent && visibleRangeStart === 0 && canonicalPath) {
11651170
const parentPath = parentOf(canonicalPath)
1166-
files.push({ name: '..', path: parentPath, isDirectory: true, size: null, recursiveSize: null, modified: null })
1171+
files.push({
1172+
name: '..',
1173+
path: parentPath,
1174+
isDirectory: true,
1175+
size: null,
1176+
recursiveSize: null,
1177+
modified: null,
1178+
recursiveSizePending: null,
1179+
})
11671180
}
11681181
11691182
// Limit to 100 files max for performance
@@ -1186,6 +1199,7 @@
11861199
size: entry.size ?? null,
11871200
recursiveSize: entry.recursiveSize ?? null,
11881201
modified: entry.modifiedAt != null ? new Date(entry.modifiedAt * 1000).toISOString() : null,
1202+
recursiveSizePending: entry.recursiveSizePending ?? null,
11891203
})
11901204
}
11911205
return files

apps/desktop/src/lib/ipc/bindings.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3097,6 +3097,14 @@ export type PaneFileEntry = {
30973097
size: number | null
30983098
recursiveSize: number | null
30993099
modified: string | null
3100+
/**
3101+
* `Some(true)` while the indexer still has unprocessed writes affecting
3102+
* this directory or a descendant, so its recursive size is mid-update.
3103+
* Surfaced in `cmdr://state` as a `[size-pending]` marker so agents can
3104+
* observe the "size updating" hourglass without DOM access. `None`/`false`
3105+
* once the writer drains. Only meaningful for directories.
3106+
*/
3107+
recursiveSizePending: boolean | null
31003108
}
31013109

31023110
// State of a single pane.

0 commit comments

Comments
 (0)