Skip to content

Commit fa0a377

Browse files
authored
Merge pull request #90 from codeflash-ai/initial-mount-file-progress
Show initial mount file indexing progress
2 parents 0bede7e + e316f4e commit fa0a377

10 files changed

Lines changed: 266 additions & 7 deletions

File tree

apps/desktop/src-tauri/src/main.rs

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,10 +225,19 @@ struct MountSummary {
225225
status: String,
226226
root_exists: bool,
227227
entity_count: usize,
228+
hydration_progress: Option<MountHydrationProgress>,
228229
pending_change_count: usize,
229230
provider: Option<ProviderRuntimeSummary>,
230231
}
231232

233+
#[derive(Clone, Serialize)]
234+
#[serde(rename_all = "camelCase")]
235+
struct MountHydrationProgress {
236+
indexed_files: usize,
237+
remaining_files: usize,
238+
total_files: usize,
239+
}
240+
232241
#[derive(Clone, Serialize)]
233242
#[serde(rename_all = "camelCase")]
234243
struct ProviderRuntimeSummary {
@@ -3937,6 +3946,7 @@ fn degraded_snapshot(message: String) -> DesktopSnapshot {
39373946
status: "error".to_string(),
39383947
root_exists: false,
39393948
entity_count: 0,
3949+
hydration_progress: None,
39403950
pending_change_count: 0,
39413951
provider: None,
39423952
},
@@ -4122,6 +4132,7 @@ fn mount_summary_with_pending_change_count(
41224132
status: "not_mounted".to_string(),
41234133
root_exists: false,
41244134
entity_count: 0,
4135+
hydration_progress: None,
41254136
pending_change_count: 0,
41264137
provider: None,
41274138
};
@@ -4133,6 +4144,7 @@ fn mount_summary_with_pending_change_count(
41334144
.unwrap_or("ready");
41344145
let access_root = mount_access_root(mount);
41354146
let root_exists = mount_root_exists_for_desktop_summary(mount, &access_root);
4147+
let entities = store.and_then(|store| store.list_entities(&mount.mount_id).ok());
41364148

41374149
MountSummary {
41384150
mount_id: mount.mount_id.0.clone(),
@@ -4160,10 +4172,13 @@ fn mount_summary_with_pending_change_count(
41604172
read_only: mount.read_only,
41614173
status: mount_status.to_string(),
41624174
root_exists,
4163-
entity_count: store
4164-
.and_then(|store| store.list_entities(&mount.mount_id).ok())
4175+
entity_count: entities
4176+
.as_ref()
41654177
.map(|entities| entities.len())
41664178
.unwrap_or(0),
4179+
hydration_progress: entities
4180+
.as_ref()
4181+
.and_then(|entities| mount_hydration_progress(entities)),
41674182
pending_change_count: pending_change_count.unwrap_or_else(|| {
41684183
store
41694184
.map(|store| pending_changes_for_mount(store, state_root, &mount.mount_id))
@@ -4175,6 +4190,33 @@ fn mount_summary_with_pending_change_count(
41754190
}
41764191
}
41774192

4193+
fn mount_hydration_progress(entities: &[EntityRecord]) -> Option<MountHydrationProgress> {
4194+
let mut indexed_files = 0usize;
4195+
let mut remaining_files = 0usize;
4196+
4197+
for entity in entities {
4198+
if entity.kind != EntityKind::Page {
4199+
continue;
4200+
}
4201+
match entity.hydration {
4202+
HydrationState::Hydrated | HydrationState::Dirty | HydrationState::Conflicted => {
4203+
indexed_files += 1;
4204+
}
4205+
HydrationState::Stub => {
4206+
remaining_files += 1;
4207+
}
4208+
HydrationState::Virtual => {}
4209+
}
4210+
}
4211+
4212+
let total_files = indexed_files + remaining_files;
4213+
(total_files > 0).then_some(MountHydrationProgress {
4214+
indexed_files,
4215+
remaining_files,
4216+
total_files,
4217+
})
4218+
}
4219+
41784220
fn mount_live_mode_summary(
41794221
store: &SqliteStateStore,
41804222
mount: Option<&MountConfig>,
@@ -11603,6 +11645,7 @@ mod tests {
1160311645
let summary = super::mount_summary(None, Path::new("."), None, None, None);
1160411646

1160511647
assert!(Path::new(&summary.local_path).is_absolute());
11648+
assert!(summary.hydration_progress.is_none());
1160611649
}
1160711650

1160811651
#[test]
@@ -11793,6 +11836,95 @@ mod tests {
1179311836
assert!(snapshot.recent_files.is_empty());
1179411837
}
1179511838

11839+
#[test]
11840+
fn desktop_snapshot_reports_hydratable_file_progress() {
11841+
let temp = TestTempDir::new("desktop-file-progress");
11842+
let mut store = SqliteStateStore::open(temp.path().to_path_buf()).expect("open store");
11843+
let connection = test_connection("workspace-1", "CodeFlash");
11844+
let mount = MountConfig::new(
11845+
MountId::new("notion-main"),
11846+
"notion",
11847+
temp.path().join("codeflash-wiki"),
11848+
)
11849+
.with_connection_id(connection.connection_id.clone())
11850+
.projection(ProjectionMode::LinuxFuse);
11851+
11852+
store.save_connection(connection).expect("save connection");
11853+
store.save_mount(mount).expect("save mount");
11854+
11855+
for (remote_id, title, path, hydration) in [
11856+
(
11857+
"hydrated-page",
11858+
"Hydrated",
11859+
"Hydrated/page.md",
11860+
HydrationState::Hydrated,
11861+
),
11862+
(
11863+
"dirty-page",
11864+
"Dirty",
11865+
"Dirty/page.md",
11866+
HydrationState::Dirty,
11867+
),
11868+
(
11869+
"conflicted-page",
11870+
"Conflicted",
11871+
"Conflicted/page.md",
11872+
HydrationState::Conflicted,
11873+
),
11874+
("stub-page", "Stub", "Stub/page.md", HydrationState::Stub),
11875+
(
11876+
"virtual-page",
11877+
"Virtual",
11878+
"Virtual/page.md",
11879+
HydrationState::Virtual,
11880+
),
11881+
] {
11882+
store
11883+
.save_entity(
11884+
EntityRecord::new(
11885+
MountId::new("notion-main"),
11886+
RemoteId::new(remote_id),
11887+
EntityKind::Page,
11888+
title,
11889+
path,
11890+
)
11891+
.with_hydration(hydration),
11892+
)
11893+
.expect("save page entity");
11894+
}
11895+
11896+
for (remote_id, kind, path) in [
11897+
("directory-1", EntityKind::Directory, "Directory"),
11898+
("database-1", EntityKind::Database, "Database"),
11899+
("asset-1", EntityKind::Asset, "asset.png"),
11900+
] {
11901+
store
11902+
.save_entity(
11903+
EntityRecord::new(
11904+
MountId::new("notion-main"),
11905+
RemoteId::new(remote_id),
11906+
kind,
11907+
remote_id,
11908+
path,
11909+
)
11910+
.with_hydration(HydrationState::Stub),
11911+
)
11912+
.expect("save non-page entity");
11913+
}
11914+
11915+
let snapshot = super::load_desktop_snapshot_from_store(&store, temp.path())
11916+
.expect("load snapshot from test store");
11917+
let progress = snapshot
11918+
.mount
11919+
.hydration_progress
11920+
.expect("file progress is reported");
11921+
11922+
assert_eq!(snapshot.mount.entity_count, 8);
11923+
assert_eq!(progress.indexed_files, 3);
11924+
assert_eq!(progress.remaining_files, 1);
11925+
assert_eq!(progress.total_files, 4);
11926+
}
11927+
1179611928
#[test]
1179711929
fn desktop_snapshot_prefers_active_granola_over_stale_notion_mount() {
1179811930
let temp = TestTempDir::new("desktop-granola-first");
@@ -16152,6 +16284,11 @@ fn sample_snapshot() -> DesktopSnapshot {
1615216284
status: "ready".to_string(),
1615316285
root_exists: true,
1615416286
entity_count: 42,
16287+
hydration_progress: Some(MountHydrationProgress {
16288+
indexed_files: 16,
16289+
remaining_files: 4,
16290+
total_files: 20,
16291+
}),
1615516292
pending_change_count: 3,
1615616293
provider: None,
1615716294
};

apps/desktop/src/App.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ import { useEffect, useMemo, useRef, useState } from "react";
4242
import {
4343
compactPath,
4444
mountEntityCountLabel,
45+
mountFileIndexProgressLabel,
46+
mountFileIndexProgressValue,
4547
mountAccessLabel,
4648
mountRows,
4749
mountStatusLabel,
@@ -382,6 +384,11 @@ const sampleMount: MountSummary = {
382384
status: "ready",
383385
rootExists: true,
384386
entityCount: 24,
387+
hydrationProgress: {
388+
indexedFiles: 16,
389+
remainingFiles: 4,
390+
totalFiles: 20,
391+
},
385392
pendingChangeCount: 3,
386393
provider: null,
387394
};
@@ -414,14 +421,14 @@ const sampleSnapshot: DesktopSnapshot = {
414421
connector: "notion",
415422
workspaceName: "CodeFlash",
416423
accountLabel: "saurabh@codeflash.ai",
417-
status: "ready",
424+
status: "active",
418425
},
419426
connections: [
420427
{
421428
connector: "notion",
422429
workspaceName: "CodeFlash",
423430
accountLabel: "saurabh@codeflash.ai",
424-
status: "ready",
431+
status: "active",
425432
},
426433
],
427434
mount: sampleMount,
@@ -4006,6 +4013,7 @@ function CurrentWorkspacePanel({
40064013
const [pullState, setPullState] = useState<"idle" | "pulling" | "success" | "error">("idle");
40074014
const accountLabel = snapshot.connection.accountLabel.trim();
40084015
const showAccount = accountLabel.length > 0 && accountLabel !== snapshot.connection.workspaceName;
4016+
const fileProgressLabel = mountFileIndexProgressLabel(snapshot.mount);
40094017

40104018
async function openFolder() {
40114019
setActionError("");
@@ -4144,7 +4152,7 @@ function CurrentWorkspacePanel({
41444152
<div className="workspace-facts">
41454153
<span>Permission: {snapshot.mount.readOnly ? "Read only" : "Edit enabled"}</span>
41464154
<span>Projection: {snapshot.mount.projection}</span>
4147-
<span>Indexed: {mountEntityCountLabel(snapshot.mount)}</span>
4155+
<span>{fileProgressLabel ?? `Indexed: ${mountEntityCountLabel(snapshot.mount)}`}</span>
41484156
{showAccount && <span>Account: {accountLabel}</span>}
41494157
</div>
41504158

@@ -4204,6 +4212,7 @@ function MountDetailView({
42044212
} = useMountLiveModeController(snapshot, onRefresh);
42054213
const liveModeAppliesToSource = isActiveMount && mount.connector === "notion";
42064214
const sourceSyncMode = sourceSyncModeLabel(snapshot.liveMode, liveModeAppliesToSource);
4215+
const fileProgressValue = mountFileIndexProgressValue(mount);
42074216

42084217
async function openFolder() {
42094218
setActionError("");
@@ -4449,6 +4458,7 @@ function MountDetailView({
44494458
<SettingRow title="Location" value={mount.localPath} />
44504459
<SettingRow title="Projection" value={mount.projection} />
44514460
<SettingRow title="Mounted content" value={`${mount.entityCount} items`} />
4461+
{fileProgressValue && <SettingRow title="Files indexed" value={fileProgressValue} />}
44524462
<SettingRow title="Root exists" value={mount.rootExists ? "Yes" : "No"} />
44534463
</div>
44544464
</section>

apps/desktop/src/dark-theme-ui.test.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ describe("dark theme UI contrast", () => {
3636
expect(styles).not.toMatch(/:root\[data-theme="dark"\] \.tray-live-mode-control,[\s\S]*?\.file-row\.expanded/s);
3737
});
3838

39+
it("keeps disabled mount detail buttons from taking hover colors", () => {
40+
expect(styles).toMatch(/\.mount-details-button:hover:not\(:disabled\)\s*\{/);
41+
expect(styles).toMatch(
42+
/:root\[data-theme="dark"\] \.mount-details-button:hover:not\(:disabled\),/,
43+
);
44+
});
45+
3946
it("uses readable control tokens for Sources page actions", () => {
4047
expect(styles).toMatch(/\.secondary-button\s*\{[\s\S]*?background:\s*var\(--control-bg\);[\s\S]*?color:\s*var\(--control-text\);/s);
4148
expect(styles).toMatch(/\.mount-details-button\s*\{[\s\S]*?background:\s*var\(--control-bg\);[\s\S]*?color:\s*var\(--control-text\);/s);

apps/desktop/src/mounts.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
22
import {
33
compactPath,
44
mountEntityCountLabel,
5+
mountFileIndexProgressLabel,
56
mountAccessLabel,
67
mountRows,
78
mountStatusLabel,
@@ -96,6 +97,68 @@ describe("mount display helpers", () => {
9697
expect(mountEntityCountLabel(mount({ entityCount: 260 }))).toBe("260 items");
9798
});
9899

100+
it("labels hydratable file indexing progress", () => {
101+
expect(
102+
mountFileIndexProgressLabel(
103+
mount({
104+
hydrationProgress: {
105+
indexedFiles: 1,
106+
remainingFiles: 2,
107+
totalFiles: 3,
108+
},
109+
}),
110+
),
111+
).toBe("Indexed: 1 of 3 files, 2 left");
112+
expect(
113+
mountFileIndexProgressLabel(
114+
mount({
115+
hydrationProgress: {
116+
indexedFiles: 1,
117+
remainingFiles: 0,
118+
totalFiles: 1,
119+
},
120+
}),
121+
),
122+
).toBe("Indexed: 1 of 1 file");
123+
expect(mountFileIndexProgressLabel(mount({}))).toBeNull();
124+
});
125+
126+
it("uses hydratable file progress as source card content while files remain", () => {
127+
const rows = mountRows(
128+
[
129+
mount({
130+
hydrationProgress: {
131+
indexedFiles: 12,
132+
remainingFiles: 68,
133+
totalFiles: 80,
134+
},
135+
}),
136+
],
137+
mount({}),
138+
"notion-main",
139+
);
140+
141+
expect(rows[0].content).toBe("Indexed: 12 of 80 files, 68 left");
142+
});
143+
144+
it("keeps compact source card content once file indexing is complete", () => {
145+
const rows = mountRows(
146+
[
147+
mount({
148+
hydrationProgress: {
149+
indexedFiles: 80,
150+
remainingFiles: 0,
151+
totalFiles: 80,
152+
},
153+
}),
154+
],
155+
mount({}),
156+
"notion-main",
157+
);
158+
159+
expect(rows[0].content).toBe("24 items, 3 pending");
160+
});
161+
99162
it("compacts long paths from the middle so filenames remain visible", () => {
100163
expect(compactPath("/home/ada/Locality/notion/Engineering/Roadmap 2026/page.md", 42)).toBe(
101164
"/.../Engineering/Roadmap 2026/page.md",

0 commit comments

Comments
 (0)