Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
f2256c6
feat: folder compare view with Off/Auto/On modes
dw-Aleksandar Apr 28, 2026
5a69c09
feat: navigate above the drive root to a Drives view
dw-Aleksandar Apr 28, 2026
65e49b0
fix: tolerate stat() failures on protected entries when listing a dir…
dw-Aleksandar Apr 28, 2026
d5086af
change: back/forward buttons follow visit history instead of walking up
dw-Aleksandar Apr 28, 2026
f4ed3b5
change: compare by size only, drop modified-timestamp check
dw-Aleksandar Apr 28, 2026
3200a16
fix: fall back to homedir when saved local path is unavailable
dw-Aleksandar Apr 29, 2026
1f11a99
feat: sync navigation — navigate both panes together when structure m…
dw-Aleksandar Apr 29, 2026
6b307a6
feat: sync back/forward history navigation across both panes
dw-Aleksandar Apr 29, 2026
ac86653
feat: sync Up, Refresh, and breadcrumb navigation across both panes
dw-Aleksandar Apr 29, 2026
17cabfb
fix: remove teal sync highlight to avoid clashing with compare colors
dw-Aleksandar Apr 29, 2026
332a616
refactor: replace Compare segmented control with single toggle button
dw-Aleksandar Apr 29, 2026
c76adae
fix: always show Sync button; auto-mode behavior when structures match
dw-Aleksandar Apr 29, 2026
fc70c92
fix: strip trailing slash in getDwRelativeTail before regex match
dw-Aleksandar Apr 29, 2026
5dae0fc
refactor: move Sync button to end of toolbar after diff pills
dw-Aleksandar Apr 29, 2026
20dc7cb
fix: clarify Sync button tooltip wording
dw-Aleksandar Apr 29, 2026
43e173b
feat: add icon and improve tooltip for Compare button
dw-Aleksandar Apr 29, 2026
efd5989
feat: highlight matching folder in opposite pane on selection when sy…
dw-Aleksandar Apr 29, 2026
37840b0
fix: correct swapped mirror paths between local and remote FileList
dw-Aleksandar Apr 29, 2026
c077055
refactor: reuse selection style for sync mirror highlight
dw-Aleksandar Apr 29, 2026
6f855fb
refactor: shorten pill labels from 'remote only'/'local only' to 'rem…
dw-Aleksandar Apr 29, 2026
a37e3c2
refactor: rename 'identical' pill label to 'equal'
dw-Aleksandar Apr 29, 2026
bc1abe5
refactor: align Compare and Sync button style with ThemeSwitcher
dw-Aleksandar Apr 29, 2026
8c4006b
fix: default compareMode to 'off' when no localStorage value exists
dw-Aleksandar Apr 29, 2026
f3ccdaa
fix: update Compare button tooltip to 'Enable/Disable file size compa…
dw-Aleksandar Apr 29, 2026
425f48d
revert: restore original Compare button tooltip
dw-Aleksandar Apr 29, 2026
4a976b5
fix: decouple pathsInSync detection from compareMode
dw-Aleksandar Apr 29, 2026
aacdd8f
fix: compute sync candidates from entries when compare is off
dw-Aleksandar Apr 29, 2026
9369fc0
fix: make back/forward navigation per-pane only, remove sync coupling
dw-Aleksandar Apr 29, 2026
3dec378
refactor: rename Sync to Mirror throughout
dw-Aleksandar Apr 29, 2026
56c4d7c
feat: mirror history stack for back/forward navigation
dw-Aleksandar Apr 29, 2026
1bd6986
refactor: simplify back/forward — use per-pane stacks, couple when mi…
dw-Aleksandar Apr 29, 2026
2b1f1c9
fix: back/forward always couples both panes when mirror is on
dw-Aleksandar Apr 29, 2026
735a9b6
fix: back/forward couples panes only when currently in sync
dw-Aleksandar Apr 29, 2026
a73bd1c
fix: deduplicate adjacent entries in back/forward stacks
dw-Aleksandar Apr 29, 2026
3a05964
fix: restructure go-back/forward to eliminate stale-closure duplicates
dw-Aleksandar Apr 29, 2026
266fc0c
feat: striped inactive state on Compare and Mirror buttons
dw-Aleksandar Apr 30, 2026
f0dec45
feat: outlined inactive state on Compare and Mirror buttons when path…
dw-Aleksandar Apr 30, 2026
5edce3e
feat: eye filter button on Compare — filter panes to selected statuses
dw-Aleksandar Apr 30, 2026
7c65867
feat: Mirror path-match buttons and Compare eye filter polish
dw-Aleksandar Apr 30, 2026
cf5dc1c
feat: expand/collapse animation for toolbar pills and sub-buttons
dw-Aleksandar Apr 30, 2026
46978cf
feat: compare toolbar polish and back/forward history improvements
dw-Aleksandar Apr 30, 2026
f8ac019
feat: breadcrumb path editor, mirror navigation polish, and UI feedba…
dw-Aleksandar Apr 30, 2026
cf503fe
Backwards compatibility for breadcrumb path slash type changes
dw-Aleksandar Apr 30, 2026
71c3ced
fix: breadcrumb path edit double-strips /Files prefix on remote pane
dw-Aleksandar Apr 30, 2026
5e3f4e9
fix: up button disabled at pane root, eye filter ignores invisible pills
dw-Aleksandar Apr 30, 2026
d361fea
fix: remove upDisabled from local pane (only remote root needs it)
dw-Aleksandar Apr 30, 2026
5caf07e
fix: localParentPath breaks on forward-slash Windows paths
dw-Aleksandar Apr 30, 2026
6f460c3
feat: disable remote-to-local arrow when local path does not exist
dw-Aleksandar Apr 30, 2026
2d96e34
fix: mirror mode case-insensitive folder matching
dw-Aleksandar Apr 30, 2026
b06e509
docs: add Compare and Mirror to README, reorder key features
dw-Aleksandar Apr 30, 2026
7340910
chore: remove Folder compare view from TODOS-ForReview (shipped)
dw-Aleksandar Apr 30, 2026
743a33c
feat: teal breadcrumb highlight when mirror is active
dw-Aleksandar Apr 30, 2026
15ea044
feat: mirror-active breadcrumb highlight with per-pane accent color
dw-Aleksandar Apr 30, 2026
68f872a
feat: truncate env name and host in pane header with tooltip
dw-Aleksandar Apr 30, 2026
261557e
feat: responsive pane header — env name compresses to give breadcrumb…
dw-Aleksandar Apr 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Get the latest version from the [Releases page](https://github.com/dynamicweb/dw

- Move files between your local machine and DW10 environments
- Browse and manage remote files visually
- Compare local and remote folder contents at a glance and filter to only files that differ
- Work across multiple environments (dev, staging, production)
- Handle large transfers with progress tracking and logs

Expand All @@ -35,6 +36,12 @@ Get the latest version from the [Releases page](https://github.com/dynamicweb/dw
- **Dual-pane file browser**
Local files on one side, remote environment on the other

- **Mirror navigation**
Navigate both panes together when folder structures match; arrow buttons snap either pane to the other's path

- **Compare mode**
Colour-coded diff between local and remote folders — different, local-only, remote-only, identical — with per-status filters

- **Drag & drop transfers**
Upload and download files with real-time progress

Expand Down
31 changes: 0 additions & 31 deletions dw-desktop/TODOS-ForReview.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,37 +45,6 @@ handling.

---

### Folder compare view

**What:** A side-by-side diff view that highlights which files differ between the
local and remote pane (by name, size, modified date). Color-code: only-on-left,
only-on-right, both-but-different, identical.

**Why:** This is the single most common question a partner asks during deployment:
"is my remote actually up to date with my local build?" Today there is no answer
without manually downloading and diffing.

**Pros:** Pure client-side feature on already-listed entries. Acts as a direct
precursor to folder sync — once you can compare, sync is the obvious follow-up.

**Cons:** No content hashing in the API — comparison is limited to name,
`sizeInBytes`, and `updatedAt` (all confirmed returned by `AssetsByDirectory`).
This is sufficient for typical deployment validation but won't catch a file
modified and then reverted to the exact same byte size.

**Context:** The existing dual-pane layout is structurally perfect for this — the
comparison is a styling overlay on existing FileList rows. All required metadata
(`sizeInBytes`, `updatedAt`) is already fetched and stored in `FileEntry`.

**Value:** Very High — directly answers the primary deployment question; transforms
the app from "file browser" to "deployment tool".

**Difficulty:** Low-Medium — all the data already exists in loaded `FileEntry`
arrays; no new API calls needed. The work is purely visual (diff overlay on
FileList rows) plus straightforward name+size+mtime comparison logic.

---

### Keyboard shortcuts

**What:** Add shortcuts for common actions: upload (Cmd/Ctrl+U), download
Expand Down
49 changes: 42 additions & 7 deletions dw-desktop/src/main/ipc/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,21 @@ async function listLocalEntries(dirPath: string): Promise<FileEntry[]> {
const entries = await Promise.all(
children.map(async (child) => {
const childPath = join(dirPath, child.name)
const childStat = await stat(childPath)
return {
name: child.name,
path: childPath,
type: (child.isDirectory() ? 'directory' : 'file') as 'file' | 'directory',
size: childStat.isFile() ? childStat.size : undefined,
modified: childStat.mtime.toISOString()
const type: 'file' | 'directory' = child.isDirectory() ? 'directory' : 'file'
// stat() can fail on protected entries at the drive root (System Volume
// Information, $Recycle.Bin, pagefile.sys, etc). Fall back to dirent info
// so a single inaccessible entry doesn't break the whole listing.
try {
const childStat = await stat(childPath)
return {
name: child.name,
path: childPath,
type,
size: childStat.isFile() ? childStat.size : undefined,
modified: childStat.mtime.toISOString()
}
} catch {
return { name: child.name, path: childPath, type }
}
})
)
Expand All @@ -35,6 +43,26 @@ async function listLocalEntries(dirPath: string): Promise<FileEntry[]> {
})
}

/**
* Enumerates available drives on Windows (A:\ through Z:\). Detects each by
* attempting `stat()` on its root in parallel. Empty array on non-Windows.
*/
async function listWindowsDrives(): Promise<FileEntry[]> {
if (process.platform !== 'win32') return []
const checks: Promise<FileEntry | null>[] = []
for (let code = 'A'.charCodeAt(0); code <= 'Z'.charCodeAt(0); code++) {
const letter = String.fromCharCode(code)
const root = `${letter}:\\`
checks.push(
stat(root)
.then(() => ({ name: `${letter}:`, path: root, type: 'directory' as const }))
.catch(() => null)
)
}
const results = await Promise.all(checks)
return results.filter((d): d is FileEntry => d !== null)
}

export function registerFileHandlers(): void {
ipcMain.handle('files:list', async (_event, { envName, path }: { envName: string; path: string }) => {
const result = getEnvOrError(envName)
Expand Down Expand Up @@ -108,6 +136,13 @@ export function registerFileHandlers(): void {

ipcMain.handle('fs:list', async (_event, { dirPath }: { dirPath: string }): Promise<IPCResult<FileEntry[]>> => {
try {
// Empty path = "above the drive root" view (Windows drives or Unix /).
if (!dirPath) {
if (process.platform === 'win32') {
return { ok: true, data: await listWindowsDrives() }
}
return { ok: true, data: await listLocalEntries('/') }
}
return { ok: true, data: await listLocalEntries(dirPath) }
} catch (err) {
return { ok: false, error: (err as Error).message }
Expand Down
2 changes: 1 addition & 1 deletion dw-desktop/src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export default function App(): React.JSX.Element {
key={t}
type="button"
onClick={() => setTab(t)}
className="no-drag"
className="no-drag nav-btn"
style={{
padding: '5px 10px',
fontSize: 12,
Expand Down
17 changes: 17 additions & 0 deletions dw-desktop/src/renderer/src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,23 @@ input:not(:focus-visible) { outline: none; }
-webkit-app-region: no-drag;
}

/* Top navigation tab button (Files, Transfer log, Debug, …) */
.nav-btn:hover {
background: var(--control-hover) !important;
color: var(--text) !important;
}
.nav-btn:active {
background: var(--surface-raised) !important;
}

/* Segmented toolbar button (Compare, Mirror, Theme, pills) */
.toolbar-btn:hover:not([disabled]) {
filter: brightness(1.12);
}
.toolbar-btn:active:not([disabled]) {
filter: brightness(0.88);
}

/* Small icon button (pane headers, toolbars) */
.icon-btn {
display: inline-flex;
Expand Down
15 changes: 11 additions & 4 deletions dw-desktop/src/renderer/src/components/AddEnvModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useEnvStore } from '../stores/envStore'
import type { StoredEnv } from '../../../shared/types'

Expand Down Expand Up @@ -97,10 +97,17 @@ export default function AddEnvModal({ onDone }: AddEnvModalProps): React.JSX.Ele
}
}

const pickingRef = useRef(false)
async function pickLocalStartPath(): Promise<void> {
const result = await window.dw.fs.openDialog(['openDirectory'])
if (result.ok && result.data && result.data.paths.length > 0) {
setStep1((s) => ({ ...s, localStartPath: result.data!.paths[0] }))
if (pickingRef.current) return
pickingRef.current = true
try {
const result = await window.dw.fs.openDialog(['openDirectory'])
if (result.ok && result.data && result.data.paths.length > 0) {
setStep1((s) => ({ ...s, localStartPath: result.data!.paths[0] }))
}
} finally {
pickingRef.current = false
}
}

Expand Down
Loading