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 file browser's plain-array navigation history with an event-driven NavStack class (EventTarget subclass) and introduces a classic .. parent-directory row in folder listings. State persistence, navbar breadcrumb updates, and actionStack cleanup are now driven by \"push\" / \"pop\" events rather than tightly-coupled imperative loops.

  • NavStack engine (NavStack.js): new class with O(1) duplicate detection via a private Set, negative-index get(), popUntil(), and native toJSON() serialization; event listeners replace all direct DOM mutations previously scattered through navigate.
  • FileBrowser wiring (fileBrowser.js): navigate and loadStates delegate history bookkeeping entirely to navStack; setSelectionMode and handleClick gain notSelectable guards for the new .. and administrative rows; $openFolder disable logic is consolidated from two branches into one line.
  • Template (list.hbs): two new boolean attributes (data-not-selectable, data-one-dir-up) are conditionally rendered on list items.

Confidence Score: 4/5

Safe to merge after verifying the two edge cases noted — no crashes or data-loss paths introduced.

The refactor is well-contained: NavStack's event-driven design cleanly decouples state from DOM, popUntil is guarded by a has() pre-check before every call, contextMenuHandler already returns early for isOneDirUp entries, and createFileStructure argument semantics are preserved. The only concrete defects are a copy-paste error message in NavStack.get() and the .. row being silently non-functional when navStack contains only one entry.

The .. insertion logic in getDir (fileBrowser.js ~line 1474) warrants a second look: the entry is unconditionally added whenever lsDir succeeds, but it only navigates correctly when the history stack has at least two entries.

Important Files Changed

Filename Overview
src/pages/fileBrowser/NavStack.js New NavStack class extending EventTarget — well-structured, but get() has a wrong method name in its TypeError message (copy-pasted from popUntil).
src/pages/fileBrowser/fileBrowser.js Major refactor wiring NavStack event listeners in place of imperative state manipulation and DOM loops; adds .. parent-directory entry; consolidates openFolder disabled toggling.
src/pages/fileBrowser/list.hbs Adds data-not-selectable and data-one-dir-up HTML attributes to the list item template; straightforward and correct.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant U as User
    participant FB as FileBrowserInclude
    participant NS as NavStack
    participant LS as localStorage

    U->>FB: click folder
    FB->>NS: navigate(url, name)
    alt url already in stack
        FB->>NS: popUntil(url)
        NS-->>FB: emit pop x N
        FB->>FB: remove navbar items and actionStack entries
    else new url
        NS-->>FB: not in stack
    end
    FB->>FB: await getDir(url, name)
    alt not already in stack
        FB->>NS: push url and name
        NS-->>FB: emit push
        FB->>FB: pushToNavbar
        NS->>LS: saveFileBrowserState
    end
    FB->>FB: render dir

    U->>FB: click ..
    FB->>NS: get(-2)
    alt prevDir exists
        FB->>FB: navigate prevDir
    else
        FB->>FB: no-op silent
    end
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 U as User
    participant FB as FileBrowserInclude
    participant NS as NavStack
    participant LS as localStorage

    U->>FB: click folder
    FB->>NS: navigate(url, name)
    alt url already in stack
        FB->>NS: popUntil(url)
        NS-->>FB: emit pop x N
        FB->>FB: remove navbar items and actionStack entries
    else new url
        NS-->>FB: not in stack
    end
    FB->>FB: await getDir(url, name)
    alt not already in stack
        FB->>NS: push url and name
        NS-->>FB: emit push
        FB->>FB: pushToNavbar
        NS->>LS: saveFileBrowserState
    end
    FB->>FB: render dir

    U->>FB: click ..
    FB->>NS: get(-2)
    alt prevDir exists
        FB->>FB: navigate prevDir
    else
        FB->>FB: no-op silent
    end
Loading

Reviews (3): 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
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 3 times, most recently from de3a1ed to 6f52174 Compare July 18, 2026 07:56
…iven `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 6f52174 to 682762f Compare July 18, 2026 11:56
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.

1 participant