Skip to content

refactor(fileBrowser): rewrite navigation history layer with event-driven NavStack and implement parent directory navigation#2500

Open
AuDevTist1C wants to merge 1 commit into
Acode-Foundation:mainfrom
AuDevTist1C:refactor/file-browser
Open

refactor(fileBrowser): rewrite navigation history layer with event-driven NavStack and implement parent directory navigation#2500
AuDevTist1C wants to merge 1 commit into
Acode-Foundation:mainfrom
AuDevTist1C:refactor/file-browser

Conversation

@AuDevTist1C

@AuDevTist1C AuDevTist1C commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

This PR completes an architectural refactor of Acode's file browser subsystem. It eliminates legacy technical debt centered around manual array manipulations, uncoupled state synchronization, and complex imperative DOM clearing loops. By replacing these workflows with a decoupled, object-oriented state engine, this change increases navigation performance, stabilizes platform state persistence, and improves UI safety across selection workflows.

Additionally, this change addresses long-standing user experience requests by introducing a classic parent-directory ascension element ("..") to folder listings, standardizing administrative file listings, and providing automated safety flags to prevent destructive modifications during multi-selection activities.


1. Architectural Rationales & Structural Refactoring

Historically, directory history within the file browser module was maintained via a plain JavaScript array (state). While functionally straightforward, this architecture coupled state manipulation directly to imperative DOM interactions. When users navigated deep into hierarchical folders or stepped backward using top breadcrumbs, the system relied on manual validation loops that concurrently modified the state index while explicitly destroying HTML fragments. This approach created code maintenance challenges and heightened the risk of race conditions, where mismatched state-to-DOM records could cause rendering errors during slow network or storage operations.

To resolve this coupling, this PR introduces the NavStack state controller class, which extends the native EventTarget framework. By inheriting from EventTarget, NavStack functions as an isolated, event-driven micro-service dedicated solely to routing history. It maintains its data structures through private properties, utilizing a private Set (#urlSet) for immediate $O(1)$ duplicate validation alongside a private sequential record array (#arr).

Instead of modifying external layout components directly, the state engine exposes high-level history actions (push, pop, popUntil) that dispatch explicit custom events containing state payloads. The core layout manager registers isolated event handlers reacting to these notifications. Consequently, state modifications automatically propagate to the browser interface and the storage synchronization hooks, standardizing data flows throughout the component's lifecycle.


2. Technical Specification of the NavStack Engine

The NavStack engine is designed with strong parameter defensive validation and precise lifecycle tracking interfaces. Below is an overview of its core API structure:

2.1 Stack Insertion Engine (push)

The push({ url, name }) method serves as the entry gateway for navigation history tracker records.

  • Defensive Guard: It strictly ensures that incoming url arguments are resolved into non-empty strings. If a null, undefined, or empty value is passed, it halts processing immediately by throwing a descriptive TypeError.
  • Idempotency Check: It verifies the string against the internal #urlSet. If the URL has already been recorded in the active navigation history, the push operation returns early without altering the sequence, preventing duplicate navigation loops.
  • Adaptive Normalization: If the descriptive folder name parameter is missing or evaluates to an empty string, it dynamically generates an fallback string by calling Url.basename(url), falling back to the raw URL if necessary.
  • Reactive Dispatch: Once pushed to the collection, it dispatches a new CustomEvent("push") carrying the immutable snapshot payload.

2.2 Stack Evacuation Engine (#popUntil / popUntil / pop)

Stack truncation is executed through a centralized private worker method (#popUntil(url)) that handles single entries as well as multi-level structural descents.

  • Reverse Iteration Loop: The routine scans backwards from the top index of the historical array. If an explicit target URL is provided, the loop terminates as soon as that folder record is reached, preserving the underlying path history.
  • Dynamic Splicing & Purging: For every entry traversed during truncation, the engine removes the unique record identifier from the validation #urlSet, cuts down the structural length of the underlying history array, and dispatches a dedicated CustomEvent("pop") detailing the removed layer.
  • Public Aliasing: The public interface splits this functionality into a single-step pop() operation and a validated popUntil(url) method. The latter includes runtime parameter guards that throw a TypeError if a blank query string is supplied.

2.3 Accessor and Serializer Utilities

  • Safe Indexing (get): The get(i) method incorporates relative negative tracking notation (e.g., passing -1 reads the top active record, while -2 retrieves the immediate parent folder). It features rigorous numeric validation via validation checks against NaN parameters and returns shallow object copies to prevent external code from mutating internal state arrays.
  • Persistence Serialization (toJSON): To allow clean integrations with local storage systems, the class exposes a native toJSON() interface. When passed into standard serialization mechanisms like JSON.stringify(), the instance automatically exports its private location list as a clean JSON structure, keeping its internal tracking mechanisms encapsulated.

3. Reactive UI Binding and State Synchronization Logic

Following the integration of the NavStack architecture, the layout controller within FileBrowserInclude has been converted from an imperative architecture into a collection of reactive event listeners.

3.1 Local Storage Synchronization

The initialization loop registers a persistent serialization pipeline linked to the state engine's lifecycle hooks. When the application configuration allows state preservation (doesOpenLast), an automated serialization task attaches directly to the "push" and "pop" events. Any modification to the directory path instantly updates the localStorage.fileBrowserState value. This setup isolates data persistence tasks from folder navigation logic, ensuring the layout remains synchronized across application updates.

3.2 Automated Layout Purging

Manually managed UI cleaning procedures inside the directory navigation module have been replaced with a clean subscriber pattern:

  • The "pop" Interface Lifecycle: When a location is evicted from history, the engine fires a pop handler. This event automatically searches for the exact DOM element via identifier queries (tag.get('#' + getNavId(url))) and removes it from the browser view. Concurrently, it automatically clears tracking indexes from the global actionStack.
  • The "push" Interface Lifecycle: When entering a new directory, a push event listener calculates the relative location step by querying the previous index via navStack.get(-2). If found, it establishes a back-navigation callback link and appends a clean navigation breadcrumb item to the upper navbar layout.

4. Implementation of Native Parent Directory Traversal ("..")

Screenshot_20260717-174212_Acode

This PR introduces a native parent folder row ("..") to the internal directory file listings. This provides an alternative to the top breadcrumbs, offering a classic directory ascension model suitable for both touchscreen interactions and keyboard navigation.

4.1 Dynamic Directory Ingestion

Within the core file rendering query engine (getDir), when a storage location successfully performs an asynchronous listing check (fs.lsDir()), a flag (oneDirUp = true) is initialized. Upon verification, the loader intercepts the folder result array and calls an ingestion routine using util.pushFolder. This creates a pseudo-directory node mapped directly to a parent directory configuration:

util.pushFolder(list, "..", null, {
    oneDirUp: true,
    notSelectable: true,
});

(PR name and description are AI generated)

@greptile-apps

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the legacy plain-array navigation state (state: Location[]) in FileBrowserInclude with a new NavStack class that extends EventTarget, wiring DOM updates and localStorage persistence to "push"/"pop" events instead of scattered imperative mutations. It also injects a .. pseudo-entry at the top of every non-root directory listing as an alternative ascension path.

  • NavStack (new file): O(1) duplicate detection via a private Set, negative-index get(), automatic name normalisation using Url.basename, and a toJSON hook for clean localStorage serialisation.
  • fileBrowser.js: loadStates is simplified to a push-loop with try/catch (fixing the previous null-entry crash); the selection-mode click guard now uses closest(\".tile\") so it correctly catches clicks on child elements; toggleSelectionMode consolidates the $openFolder.disabled toggle into a single line; and wide-ranging style cleanup reduces overall file length.
  • list.hbs: Adds the data-not-selectable and data-one-dir-up boolean HTML attributes consumed by the new JS routing.

Confidence Score: 4/5

The architectural refactor is sound and resolves several correctness issues from prior reviews; the one remaining gap is the missing break on the "refresh" switch case and the previously-flagged missing break on "oneDirUp", both harmless today but fragile under future edits.

The NavStack core and its wiring into FileBrowserInclude are well-structured. The prior TDZ crash in the selection-mode click handler is fixed, loadStates now gracefully skips malformed entries via try/catch, and name normalisation inside NavStack.push eliminates the old silent-failure path for falsy stored names. Two switch cases lack explicit terminators — neither causes a current defect since both are last in their respective switch bodies, but the pattern is fragile. No data-loss or crash path was found in the new code.

fileBrowser.js — specifically the $fbMenu.onclick switch ("refresh" case) and the handleClick switch ("oneDirUp" case), both missing break statements.

Important Files Changed

Filename Overview
src/pages/fileBrowser/NavStack.js New event-driven navigation stack extending EventTarget. Well-structured with O(1) duplicate detection, proper defensive validation, negative-index access, and JSON serialisation. No issues found.
src/pages/fileBrowser/fileBrowser.js Large refactor integrating NavStack, adding .. parent-dir row, and extensive code cleanup. The TDZ crash and loadStates name-fallback issues from previous reviews are resolved; "refresh" case is missing a break; oneDirUp case (flagged in previous review) still lacks one too.
src/pages/fileBrowser/list.hbs Adds data-not-selectable and data-one-dir-up boolean HTML attributes for the new notSelectable/oneDirUp tile flags. Attribute names correctly camelCase to dataset.notSelectable / dataset.oneDirUp as consumed by the JS. No issues found.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant FileBrowser
    participant NavStack
    participant ActionStack
    participant DOM

    User->>FileBrowser: click directory tile
    FileBrowser->>NavStack: has(url)?
    alt url already in stack (back-nav)
        NavStack-->>FileBrowser: true
        FileBrowser->>NavStack: popUntil(url)
        NavStack->>DOM: pop event → remove nav breadcrumbs
        NavStack->>ActionStack: pop event → remove(url)
    else new directory
        NavStack-->>FileBrowser: false
        FileBrowser->>FileBrowser: getDir(url, name) [async]
        FileBrowser->>NavStack: "push({url, name})"
        NavStack->>DOM: push event → pushToNavbar(name, url, backAction)
        NavStack->>ActionStack: push event → saveFileBrowserState
    end
    FileBrowser->>DOM: render(dir)

    User->>FileBrowser: click ".." entry
    FileBrowser->>NavStack: get(-2) → prevDir
    FileBrowser->>FileBrowser: navigate(prevDir.url, prevDir.name)

    User->>FileBrowser: restore on open (loadStates)
    FileBrowser->>NavStack: push "/" first
    loop stored states
        FileBrowser->>NavStack: push(state)
        NavStack->>DOM: push event → pushToNavbar
    end
    FileBrowser->>FileBrowser: navigate(last url, last name)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant FileBrowser
    participant NavStack
    participant ActionStack
    participant DOM

    User->>FileBrowser: click directory tile
    FileBrowser->>NavStack: has(url)?
    alt url already in stack (back-nav)
        NavStack-->>FileBrowser: true
        FileBrowser->>NavStack: popUntil(url)
        NavStack->>DOM: pop event → remove nav breadcrumbs
        NavStack->>ActionStack: pop event → remove(url)
    else new directory
        NavStack-->>FileBrowser: false
        FileBrowser->>FileBrowser: getDir(url, name) [async]
        FileBrowser->>NavStack: "push({url, name})"
        NavStack->>DOM: push event → pushToNavbar(name, url, backAction)
        NavStack->>ActionStack: push event → saveFileBrowserState
    end
    FileBrowser->>DOM: render(dir)

    User->>FileBrowser: click ".." entry
    FileBrowser->>NavStack: get(-2) → prevDir
    FileBrowser->>FileBrowser: navigate(prevDir.url, prevDir.name)

    User->>FileBrowser: restore on open (loadStates)
    FileBrowser->>NavStack: push "/" first
    loop stored states
        FileBrowser->>NavStack: push(state)
        NavStack->>DOM: push event → pushToNavbar
    end
    FileBrowser->>FileBrowser: navigate(last url, last name)
Loading

Reviews (8): Last reviewed commit: "refactor(fileBrowser): rewrite navigatio..." | Re-trigger Greptile

Comment thread src/pages/fileBrowser/fileBrowser.js
Comment on lines +1106 to +1111
case "oneDirUp": {
const dir = navStack.get(-2);
if (!dir) break;
const { url, name } = dir;
navigate(url, name);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 .. resolves to navigation-history parent, not the filesystem parent

navStack.get(-2) returns the previously-visited directory, not the actual URL-parent of the current directory. These are the same in linear navigation, but diverge in edge cases — e.g. if a future feature adds bookmarks or deep-links that push multiple levels to navStack at once (like loadStates already does). In that scenario pressing .. could land on a directory that is not an ancestor of the current one at all. The traditional expected behaviour of .. is Url.dirname(currentDir.url). Consider adding a clarifying comment or computing the real parent as a fallback.

Comment thread src/pages/fileBrowser/fileBrowser.js Outdated
Comment on lines +1106 to +1111
case "oneDirUp": {
const dir = navStack.get(-2);
if (!dir) break;
const { url, name } = dir;
navigate(url, name);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing break at end of oneDirUp case

The oneDirUp block has no trailing break. While this is currently safe because it is the last case, future additions to the switch will silently fall through into the new case without any visible indication that the omission is intentional. Adding break makes the intent explicit and future-proof.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@AuDevTist1C
AuDevTist1C force-pushed the refactor/file-browser branch 5 times, most recently from 682762f to b7687ee Compare July 19, 2026 22:42
@bajrangCoder

This comment was marked as outdated.

@AuDevTist1C
AuDevTist1C force-pushed the refactor/file-browser branch from 08ccd4c to 105dc80 Compare July 20, 2026 07:44
Comment thread src/pages/fileBrowser/fileBrowser.js
@AuDevTist1C
AuDevTist1C force-pushed the refactor/file-browser branch 2 times, most recently from 003df66 to b513169 Compare July 20, 2026 09:21
@greptile-apps

This comment was marked as outdated.

 event-driven `NavStack` and implement parent directory navigation

This commit completes an architectural refactor of Acode's file browser subsystem. It eliminates legacy technical debt centered around manual array manipulations, uncoupled state synchronization, and complex imperative DOM clearing loops. By replacing these workflows with a decoupled, object-oriented state engine, this change increases navigation performance, stabilizes platform state persistence, and improves UI safety across selection workflows.

Additionally, this change addresses long-standing user experience requests by introducing a classic parent-directory ascension element ("..") to folder listings, standardizing administrative file listings, and providing automated safety flags to prevent destructive modifications during multi-selection activities.

---

Historically, directory history within the file browser module was maintained via a plain JavaScript array (`state`). While functionally straightforward, this architecture coupled state manipulation directly to imperative DOM interactions. When users navigated deep into hierarchical folders or stepped backward using top breadcrumbs, the system relied on manual validation loops that concurrently modified the state index while explicitly destroying HTML fragments. This approach created code maintenance challenges and heightened the risk of race conditions, where mismatched state-to-DOM records could cause rendering errors during slow network or storage operations.

To resolve this coupling, this commit introduces the `NavStack` state controller class, which extends the native `EventTarget` framework. By inheriting from `EventTarget`, `NavStack` functions as an isolated, event-driven micro-service dedicated solely to routing history. It maintains its data structures through private properties, utilizing a private `Set` (`#urlSet`) for immediate $O(1)$ duplicate validation alongside a private sequential record array (`#arr`).

Instead of modifying external layout components directly, the state engine exposes high-level history actions (`push`, `pop`, `popUntil`) that dispatch explicit custom events containing state payloads. The core layout manager registers isolated event handlers reacting to these notifications. Consequently, state modifications automatically propagate to the browser interface and the storage synchronization hooks, standardizing data flows throughout the component's lifecycle.

---

The `NavStack` engine is designed with strong parameter defensive validation and precise lifecycle tracking interfaces. Below is an overview of its core API structure:

The `push({ url, name })` method serves as the entry gateway for navigation history tracker records.
* **Defensive Guard**: It strictly ensures that incoming `url` arguments are resolved into non-empty strings. If a null, undefined, or empty value is passed, it halts processing immediately by throwing a descriptive `TypeError`.
* **Idempotency Check**: It verifies the string against the internal `#urlSet`. If the URL has already been recorded in the active navigation history, the push operation returns early without altering the sequence, preventing duplicate navigation loops.
* **Adaptive Normalization**: If the descriptive folder `name` parameter is missing or evaluates to an empty string, it dynamically generates an fallback string by calling `Url.basename(url)`, falling back to the raw URL if necessary.
* **Reactive Dispatch**: Once pushed to the collection, it dispatches a new `CustomEvent("push")` carrying the immutable snapshot payload.

Stack truncation is executed through a centralized private worker method (`#popUntil(url)`) that handles single entries as well as multi-level structural descents.
* **Reverse Iteration Loop**: The routine scans backwards from the top index of the historical array. If an explicit target URL is provided, the loop terminates as soon as that folder record is reached, preserving the underlying path history.
* **Dynamic Splicing & Purging**: For every entry traversed during truncation, the engine removes the unique record identifier from the validation `#urlSet`, cuts down the structural length of the underlying history array, and dispatches a dedicated `CustomEvent("pop")` detailing the removed layer.
* **Public Aliasing**: The public interface splits this functionality into a single-step `pop()` operation and a validated `popUntil(url)` method. The latter includes runtime parameter guards that throw a `TypeError` if a blank query string is supplied.

* **Safe Indexing (`get`)**: The `get(i)` method incorporates relative negative tracking notation (e.g., passing `-1` reads the top active record, while `-2` retrieves the immediate parent folder). It features rigorous numeric validation via validation checks against `NaN` parameters and returns shallow object copies to prevent external code from mutating internal state arrays.
* **Persistence Serialization (`toJSON`)**: To allow clean integrations with local storage systems, the class exposes a native `toJSON()` interface. When passed into standard serialization mechanisms like `JSON.stringify()`, the instance automatically exports its private location list as a clean JSON structure, keeping its internal tracking mechanisms encapsulated.

---

Following the integration of the `NavStack` architecture, the layout controller within `FileBrowserInclude` has been converted from an imperative architecture into a collection of reactive event listeners.

The initialization loop registers a persistent serialization pipeline linked to the state engine's lifecycle hooks. When the application configuration allows state preservation (`doesOpenLast`), an automated serialization task attaches directly to the `"push"` and `"pop"` events. Any modification to the directory path instantly updates the `localStorage.fileBrowserState` value. This setup isolates data persistence tasks from folder navigation logic, ensuring the layout remains synchronized across application updates.

Manually managed UI cleaning procedures inside the directory navigation module have been replaced with a clean subscriber pattern:
* **The "pop" Interface Lifecycle**: When a location is evicted from history, the engine fires a pop handler. This event automatically searches for the exact DOM element via identifier queries (`tag.get('#' + getNavId(url))`) and removes it from the browser view. Concurrently, it automatically clears tracking indexes from the global `actionStack`.
* **The "push" Interface Lifecycle**: When entering a new directory, a push event listener calculates the relative location step by querying the previous index via `navStack.get(-2)`. If found, it establishes a back-navigation callback link and appends a clean navigation breadcrumb item to the upper navbar layout.

---

This commit introduces a native parent folder row ("..") to the internal directory file listings. This provides an alternative to the top breadcrumbs, offering a classic directory ascension model suitable for both touchscreen interactions and keyboard navigation.

Within the core file rendering query engine (`getDir`), when a storage location successfully performs an asynchronous listing check (`fs.lsDir()`), a flag (`oneDirUp = true`) is initialized. Upon verification, the loader intercepts the folder result array and calls an ingestion routine using `util.pushFolder`. This creates a pseudo-directory node mapped directly to a parent directory configuration:
```javascript
util.pushFolder(list, "..", null, {
    oneDirUp: true,
    notSelectable: true,
});
```

(AI generated commit message)
@AuDevTist1C
AuDevTist1C force-pushed the refactor/file-browser branch from b513169 to 8bb4dc0 Compare July 20, 2026 09:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants