How Project Order is Managed in Nodeterm: Zustand Array-Based State

Project order in Nodeterm is managed through a Zustand store that maintains a projects array, where the visual tab sequence directly mirrors the array index order, manipulated via the reorderProject() method.

The open-source terminal emulator nodeterm uses a deterministic, array-based approach to handle project sequencing. According to the eneskirca/nodeterm source code, the application stores all open projects in a centralized Zustand store, ensuring that the tab bar, sidebar, and canvas all render projects in the exact same sequence.

The Zustand Store Architecture

Nodeterm implements its project state management in src/renderer/state/projects.ts. This file defines a ProjectsState store containing a projects: Project[] array that serves as the single source of truth for project ordering.

The visual layout of the UI layers—whether the horizontal tab bar or vertical sidebar—directly reflects the indices of this array. Index 0 represents the leftmost tab, while the final index represents the rightmost position. Because all components read from the same Zustand store, any modification to the array order instantly propagates across the entire interface.

How the reorderProject Method Works

The core logic for reordering resides in the reorderProject(draggedId, beforeId) method. This function accepts two string parameters: the ID of the project being moved and the ID of the project it should precede (or null to append to the end).

The implementation follows a three-step algorithm:

  1. Filter the dragged project from the array to create a without array.
  2. Locate the insertion index by finding the position of beforeId in the filtered array. If beforeId is null or not found, the index defaults to -1.
  3. Splice the dragged project into the computed position, appending it to the end if the index is -1.
// src/renderer/state/projects.ts (lines 49-59)
reorderProject(draggedId, beforeId) {
  set((s) => {
    if (draggedId === beforeId) return s
    const dragged = s.projects.find((p) => p.id === draggedId)
    if (!dragged) return s
    const without = s.projects.filter((p) => p.id !== draggedId)
    const idx = beforeId ? without.findIndex((p) => p.id === beforeId) : -1
    // Unknown/null target → append (the "drop at the end" zone).
    const at = idx === -1 ? without.length : idx
    return { projects: [...without.slice(0, at), dragged, ...without.slice(at)] }
  })
}

This immutable update pattern ensures React re-renders trigger correctly while preserving reference stability for the unchanged project objects.

Adding, Closing, and Deleting Projects

Project lifecycle operations affect the array differently depending on the intended persistence behavior.

Adding New Projects

When users create a project via addProject(name, cwd, ssh), the method pushes the new Project object to the end of the projects array. This automatically positions the new tab at the far right of the tab bar.

import { useProjects } from '@/renderer/state/projects'

// Adds "My New Project" to the rightmost position
const newProj = useProjects.getState().addProject('My New Project', '/path/to/folder')

Closing vs. Deleting Projects

Closing a project sets a closed: true flag on the project object but retains its position in the array. This preserves the slot for later reopening without disturbing the order of remaining active tabs.

Deleting a project mutates the array by filtering out the project entirely. When deletion occurs, the next active project is selected based on the nearest neighbor in the current array order.

Key Implementation Files

The project ordering system spans three primary locations in the codebase:

  • src/renderer/state/projects.ts – Contains the ProjectsState Zustand store, the projects array definition, and the reorderProject implementation.
  • src/renderer/state/workspace.ts – Provides utilities such as createProject that generate new Project objects before they enter the store.
  • src/shared/project-id.ts – Supplies deterministic ID generation functions (derivedProjectId, collisionSeed) used during project creation and collision resolution.

Practical Code Examples

The following patterns demonstrate common reordering operations against the live store:

import { useProjects } from '@/renderer/state/projects'

// Move "proj-2" immediately before "proj-1"
useProjects.getState().reorderProject('proj-2', 'proj-1')

// Move "proj-3" to the end of the tab bar (null target)
useProjects.getState().reorderProject('proj-3', null)

These calls trigger the immutable splicing logic and immediately update the UI without requiring explicit refresh calls.

Summary

  • Array-based sequencing: The projects array in src/renderer/state/projects.ts determines tab order by index.
  • Centralized state: Zustand ensures the tab bar, sidebar, and canvas remain synchronized.
  • Immutable reordering: The reorderProject(draggedId, beforeId) method filters, calculates insertion points, and reconstructs the array without mutating the original.
  • Append-only addition: New projects added via addProject() always initialize at the array's end (rightmost tab).
  • Soft close mechanic: Closing preserves array position via a boolean flag; deleting removes the element and triggers nearest-neighbor selection.

Frequently Asked Questions

How does Nodeterm store the order of open projects?

Nodeterm maintains the order in a Zustand store defined in src/renderer/state/projects.ts. The store contains a projects array where each element's index corresponds to its visual position in the tab bar and sidebar.

What happens when I close a project in Nodeterm?

Closing a project toggles a closed: true property on the project object but leaves it in the projects array. This preserves the project's slot and ordering information, allowing you to reopen it later without affecting the sequence of other tabs.

How do I move a project to the end of the tab bar programmatically?

Call reorderProject(draggedId, null) on the store. The null value for the second parameter signals the method to calculate the insertion index as the array length, effectively appending the project to the final position.

Where is the project state defined in the codebase?

The primary state definition lives in src/renderer/state/projects.ts. This file exports the useProjects hook and contains the ProjectsState interface, the projects array, and all mutation methods including reorderProject and addProject.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →