Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,6 @@ mise.toml

# Superset
.superset

# tsbuildinfo
*.tsbuildinfo
1 change: 0 additions & 1 deletion apps/blog/tsconfig.tsbuildinfo

This file was deleted.

11 changes: 8 additions & 3 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "cli",
"name": "@superset/cli",
"version": "0.0.0",
"license": "MIT",
"bin": "dist/cli.js",
Expand All @@ -19,10 +19,15 @@
"format:check": "biome format ."
},
"files": [
"dist"
"dist",
"src/types"
],
"exports": {
"./types/*": "./src/types/*",
"./types": "./src/types/index.ts"
},
"dependencies": {
"commander": "^14.0.1",
"commander": "^14.0.2",
"ink": "^6.5.0",
"ink-select-input": "^6.2.0",
"ink-table": "^3.1.0",
Expand Down
63 changes: 63 additions & 0 deletions apps/desktop/docs/DND_BACKEND_UNIFICATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Drag-and-Drop Backend Unification (HTML5Backend)

Problem: Multiple HTML5 backends were created at runtime, triggering:

Error: Cannot have two HTML5 backends at the same time.

This occurred because our app used React DnD directly while `react-mosaic-component` and `react-arborist` also rely on React DnD. If any of them creates its own `DndProvider`/backend, we end up with multiple HTML5 backends in the same window.

## Goals

- Enforce a single, shared DragDropManager/HTML5 backend for the entire renderer.
- Keep Mosaic and Arborist interoperable by reusing the same manager.
- Prevent regressions by documenting the pattern and providing a shared utility.

## High-Level Plan

- Create a shared DnD manager via `createDndContext(HTML5Backend)`.
- Use a single top-level `DndProvider` wired to that manager.
- Pass the same manager to any library that can optionally create its own provider (e.g., `react-arborist`’s `Tree` via `dndManager`).
- Remove/avoid any nested `DndProvider` instances that initialize their own HTML5 backend.

## Implementation Details

1) Shared manager utility
- File: `apps/desktop/src/renderer/lib/dnd.ts`
- Exports: `dragDropManager` created once via `createDndContext(HTML5Backend)`.

2) Top-level provider uses the shared manager
- File: `apps/desktop/src/renderer/screens/main/MainScreen.tsx`
- Change: Replace `backend={HTML5Backend}` with `manager={dragDropManager}` on `DndProvider`.

3) Arborist uses the shared manager
- Files:
- `apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/WorktreeItem.tsx`
- `apps/desktop/src/renderer/screens/main/components/Sidebar/components/WorktreeList/components/WorktreeItem/WorktreeItemArborist.tsx`
- Change: Add `dndManager={dragDropManager}` to `<Tree />` and import the manager from `renderer/lib/dnd`.

4) Mosaic
- `react-mosaic-component` internally mounts its own `DndProvider` using `react-dnd-multi-backend`.
- To avoid multiple MultiBackends, pass the shared manager via `dragAndDropManager={dragDropManager}` to `<Mosaic />`.
- This makes its internal provider reuse the shared manager rather than creating a new MultiBackend instance.

## What Changed (Summary)

- Added `apps/desktop/src/renderer/lib/dnd.ts` exporting a singleton `dragDropManager`.
- Updated top-level DnD provider in `MainScreen.tsx` to use `manager={dragDropManager}`.
- Updated all Arborist `Tree` usages to pass `dndManager={dragDropManager}`.

## Validation

- Typecheck/lint: `bun run typecheck` and `bun run lint:check` at repo root.
- Manual: Open the Desktop app and interact with Mosaic panes and the Arborist trees (dragging, dropping, splitting). No console errors about multiple HTML5 backends should appear.

## Regression Guardrails

- Do not add additional `DndProvider` instances in the renderer. If a subtree must have a provider for scoping, pass `manager={dragDropManager}` to reuse the shared manager.
- For `react-arborist`, always provide `dndManager={dragDropManager}` to `<Tree />`.
- Centralize DnD concerns in `renderer/lib/dnd.ts`. If backend options change (e.g., `rootElement`), update only this file.

## Notes & Alternatives

- If drag-and-drop is needed within portals or iframes, configure backend `options` (e.g., `rootElement`) in `renderer/lib/dnd.ts` and ensure every consumer still reuses the same manager.
- This approach avoids multi-backend solutions and keeps complexity low by standardizing on one HTML5 backend across the renderer.
151 changes: 151 additions & 0 deletions apps/desktop/docs/STATE_PERSISTENCE_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Desktop State & Persistence Refactor Plan

Author: Platform
Status: Proposed
Audience: Agents working on Desktop app state/persistence

## Context

The Desktop app’s current persistence mixes runtime data (worktrees) with UI and domain concerns in a single JSON file. This leads to drift (e.g., worktrees out of sync with Git) and makes the state model hard to evolve. We also want to align domain models with the CLI types while keeping CLI and Desktop storage separate for now.

## Goals

- Make Git the single source of truth for worktrees (scan, don’t trust JSON).
- Split persistence into coherent, versioned slices aligned to CLI domain types and Desktop UI.
- Keep Desktop and CLI persistence separate locations.
- Establish a deterministic loading sequence with background rescan/reconciliation.
- Preserve type safety and the type-safe IPC pattern.

## Non‑Goals

- Do not merge CLI and Desktop persistence yet.
- Do not introduce new runtime dependencies or database engines.
- Do not prescribe file‑level code changes; this plan guides architecture/tasks.

## Principles

- Decouple domain, UI, and derived state.
- Prefer immutable sources (Git) over persisted lists for worktrees.
- Persist only what cannot be derived (domain objects, UI state).
- Use existing CLI types as the canonical domain shape.

## State Domains

- Domain (persisted; aligns with CLI types)
- Environment, Workspace (LocalWorkspace for Desktop), Process/Agent/Terminal, Change, FileDiff, AgentSummary.
- UI (persisted; Desktop‑only)
- Window state, last active workspace.
- Per‑workspace UI state: active worktree selection, active tab, tabs, mosaic layout, per‑tab CWD/URL, worktree metadata (description, prUrl, merged).
- Derived (in‑memory / cache)
- Worktrees scanned from Git (branch/path), detected ports, short‑lived git status.

## Persistence Split

- CLI location remains separate (unchanged): `~/.superset/cli/`.
- Desktop location (new): `~/.superset/desktop/`
- db/ (domain; versioned)
- environments, workspaces (LocalWorkspace), processes, changes, fileDiffs, agentSummaries (split by collection).
- db.version
- ui/ (Desktop‑only; versioned)
- window-state.json, settings.json (lastActiveWorkspaceId, preferences)
- workspaces/<workspaceId>.json (per‑workspace UI state)
- ui.version
- cache/ (ephemeral)
- ports.json, optional git status caches

Note: The “split by file per collection” is to keep the “not one big JSON” requirement. If a single domain DB file is preferred later, keep collections separate within the file while UI remains separate.

## Loading Flow (High‑Level)

1. App Boot (Main)
- Load env with override (uses find-up logic to locate monorepo root .env robustly).
- Initialize domain store targeting `~/.superset/desktop/db/`.
- Load UI settings (window + last active workspace) from `~/.superset/desktop/ui/`.
2. Activate Workspace
- Read domain workspace (LocalWorkspace) by id.
- Scan Git for worktrees (path/branch/bare) using the workspace repo path.
- Merge: join scanned worktrees with per‑workspace UI metadata keyed by worktree path (fallback to branch when needed).
- Initialize defaults for new worktrees (e.g., a terminal tab) in UI state when appropriate.
- Cache the scan result as the "activation-time snapshot" for diff tracking.
- Start background tasks: periodic rescans (every 30s), port detection for terminals in the active worktree, update proxy targets.
3. Refresh
- Manual rescan via IPC and periodic rescan reconcile UI metadata with Git (remove orphans after a grace period; retain notes when possible).
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.

⚠️ Potential issue | 🟡 Minor

Resolve inconsistency: grace period for orphaned metadata.

Line 72 (Refresh section) describes orphan removal "after a grace period" as settled behavior, but line 145 (Open Questions) lists "Grace period policy for orphaned UI metadata (immediate prune vs delayed cleanup)?" as an unanswered question.

Clarify whether a grace period is decided and documented here, or whether it remains open for the implementation team to decide.

Also applies to: 145-145

🤖 Prompt for AI Agents
In apps/desktop/docs/STATE_PERSISTENCE_PLAN.md at lines 72 and 145, the doc
currently asserts orphaned UI metadata is removed "after a grace period" (line
72) but later lists the grace period policy as an open question (line 145);
reconcile these by either (A) deciding the policy and updating line 72 to state
the chosen grace period and rationale and removing the "open question" entry at
line 145, or (B) if undecided, change line 72 to a tentative wording (e.g., "may
be removed pending grace period policy") and keep line 145 as an active open
question; apply the corresponding text edits so both locations consistently
reflect the final decision and include any chosen duration, retention rules, and
who owns the decision.

- **Important**: The first rescan after activation produces diffs relative to the activation-time snapshot only. Therefore, `workspace-activate` must be called once before `workspace-rescan` for meaningful diffs. If the renderer triggers a rescan before activation finishes, diffs will be empty.

## Worktree Strategy

- Always treat Git as truth for current worktrees.
- Never load authoritative worktree lists from persistence.
- Maintain per‑worktree UI metadata keyed by worktree path (primary) and branch (secondary) to survive path changes or renames.

## IPC Contracts (Conceptual)

- Workspace
- workspace.activate: { workspaceId } → composed state (domain workspace + scanned worktrees + UI state)
- workspace.rescan: { workspaceId } → rescan result (diff + composed state)
- UI
- ui.workspace.get: { workspaceId } → current per‑workspace UI state
- ui.workspace.update: { workspaceId, patch } → update specific UI fields
- ui.set-active: { workspaceId, activeWorktreePath?, activeTabId? } → updates global active workspace and per-workspace active worktree/tab
- **Important**: Renderer must call `ui.set-active` when switching workspaces to ensure `lastActiveWorkspaceId` is persisted. This ensures the correct workspace is restored on next launch.
- Processes (domain)
- process.list/create/stop/stopAll following CLI type semantics

Use existing type‑safe IPC conventions (object params; shared channel type definitions). Exact channel names and types should be captured in the shared IPC types file before implementing.

## Migration Strategy (One‑Time)

- Trigger: Detect legacy `~/.superset/config.json` and an empty `~/.superset/desktop/`.
- Mapping
- Desktop Workspace → Domain LocalWorkspace (id preserved; repoPath → path; type=local; environmentId=default).
- Worktrees: stop persisting worktree arrays as authoritative; create per‑workspace UI metadata keyed by worktree path (description, prUrl, merged, tabs, mosaic layout).
- Active selection: move to UI settings (lastActiveWorkspaceId) and per‑workspace UI (`activeWorktreePath`, `activeTabId`).
- Window/layout prefs: move to UI/window state.
- Versioning and Safety
- Initialize `db.version=1` and `ui.version=1`.
- Atomic writes with backup of legacy file.
- Validate schemas; skip/record invalid entries.

## Risks & Mitigations

- Worktree rename/path changes: primary key by path, secondary by branch; prompt on ambiguity.
- Large repos: throttle rescans; make resumable/cancellable; limit scope to worktrees rather than full repo.
- Partial writes: **Implemented** - UI store uses atomic write pattern (write to *.tmp, fsync, rename) for all persistence operations (window-state.json, settings.json, per-workspace UI state). This prevents data corruption on crash.
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.

⚠️ Potential issue | 🟡 Minor

Clarify implementation status of atomic write pattern.

Line 113 claims the atomic write pattern is "Implemented," but this is a Proposed plan document. Either this pattern was implemented in a prior commit (if so, reference it) or remove the "Implemented" marker to avoid confusion about what is currently in place vs. planned.

🤖 Prompt for AI Agents
In apps/desktop/docs/STATE_PERSISTENCE_PLAN.md around line 113, the document
incorrectly labels the atomic write pattern as "Implemented"; verify if the
pattern already exists by searching commits/PRs and, if found, update the line
to include a reference to the implementing commit or PR (hash/URL and file path)
and a brief note about where the implementation lives; if no implementation
exists, remove the "Implemented" marker and change the wording to "Proposed" or
"Planned" (and optionally include an action item/ticket reference) so the
document accurately reflects the current status.

- Port detection flapping: debounce updates; only persist stable snapshots in UI or cache.

## Milestones

- M1: Establish Desktop domain store (separate root) and UI store scaffolding; no behavior change.
- M2: Compose workspace state from Git + UI (stop depending on persisted worktree arrays).
- M3: IPC endpoints for activation/rescan/UI updates exposed; renderer consumes composed state.
- M4: Migration path from legacy config with backups and versioning.
- M5: Background rescans + port/proxy refresh handling; logs and metrics.

## Task Checklist (Agent‑Oriented)

- [ ] Define Desktop domain store interfaces aligning with CLI types; point storage root to `~/.superset/desktop/db/`.
- [ ] Define UI store for `~/.superset/desktop/ui/` with schemas for window, settings, and per‑workspace UI.
- [ ] Implement composition logic: read domain workspace → scan Git → merge with UI metadata by worktree path.
- [ ] Add a periodic rescan strategy (interval + manual trigger) and reconciliation rules.
- [ ] Specify IPC channel contracts in shared IPC types for workspace activate/rescan and UI get/update.
- [ ] Implement migration runner (detect legacy file, map to new domain+UI slices, write backups, set versions).
- [ ] Add structured logs for scans, merges, and migrations; include a “dry run” mode for migration.
- [ ] Validate with sample repos and multi‑worktree setups; confirm no reliance on persisted worktree arrays.

## Validation & Observability

- Unit and integration checks for composition (new/missing/renamed worktrees).
- Migration dry‑run and post‑migration verification (counts, ids, schema validation).
- Log key events: activation, rescan diff, migration start/end, errors.
- Optional telemetry counters (worktrees detected, UI orphans pruned) if allowed by the project.

## Open Questions

- Should ports configuration be domain (shared) or UI (Desktop‑only)? For now, keep it in UI; revisit if multiple apps share it.
- Grace period policy for orphaned UI metadata (immediate prune vs delayed cleanup)?
- Desired default tab layout for newly detected worktrees?

---

Implementation may proceed behind a feature flag or staged rollout per milestones above. This document intentionally avoids prescribing specific file edits; it defines outcomes, boundaries, and tasks for agents to execute.

12 changes: 11 additions & 1 deletion apps/desktop/electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ const tsconfigPaths = tsconfigPathsPlugin({

export default defineConfig({
main: {
plugins: [tsconfigPaths, externalizeDepsPlugin()],
plugins: [
tsconfigPaths,
externalizeDepsPlugin({
exclude: ["@superset/cli"],
}),
],

build: {
rollupOptions: {
Expand All @@ -35,6 +40,11 @@ export default defineConfig({
},
},
},
resolve: {
alias: {
"@superset/cli": resolve(__dirname, "../../apps/cli/src"),
},
},
},

preload: {
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,20 @@
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@superset/api": "workspace:*",
"@superset/cli": "workspace:*",
"@superset/ui": "workspace:*",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-search": "^0.15.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/addon-webgl": "^0.18.0",
"@xterm/xterm": "^5.5.0",
"clsx": "^2.1.1",
"dotenv": "^17.2.3",
"electron-router-dom": "^2.1.0",
"fast-glob": "^3.3.3",
"framer-motion": "^12.23.24",
"http-proxy": "^1.18.1",
"lowdb": "^7.0.1",
"lucide-react": "^0.553.0",
"node-pty": "1.1.0-beta30",
"react": "^19.1.1",
Expand All @@ -70,7 +73,6 @@
"@vitejs/plugin-react": "^5.0.1",
"code-inspector-plugin": "^1.2.2",
"cross-env": "^10.0.0",
"dotenv": "^17.2.3",
"electron": "^39.1.2",
"electron-builder": "^26.0.12",
"electron-extension-installer": "^2.0.0",
Expand Down
41 changes: 38 additions & 3 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
// Load .env from monorepo root before any other imports
import { resolve } from "node:path";
import { existsSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { config } from "dotenv";

// Use override: true to ensure .env values take precedence over inherited env vars
config({ path: resolve(__dirname, "../../../../.env"), override: true });
// Find .env file by searching upward from __dirname
// This is robust whether running from source or compiled code
function findEnvFile(): string | undefined {
let currentDir = __dirname;
for (let i = 0; i < 6; i++) {
const envPath = resolve(currentDir, ".env");
if (existsSync(envPath)) {
return envPath;
}
currentDir = dirname(currentDir);
}
return undefined;
}

const envPath = findEnvFile();
if (envPath) {
// Use override: true to ensure .env values take precedence over inherited env vars
config({ path: envPath, override: true });
console.log(`Loaded .env from ${envPath}`);
} else {
console.warn("No .env file found in parent directories");
}

import path from "node:path";
import { app } from "electron";
Expand All @@ -12,6 +33,7 @@ import { registerDeepLinkIpcs } from "main/lib/deep-link-ipcs";
import { deepLinkManager } from "main/lib/deep-link-manager";
import { registerPortIpcs } from "main/lib/port-ipcs";
import { getPort } from "main/lib/port-manager";
import { registerUiIPCs } from "main/lib/ui-ipcs";
import windowManager from "main/lib/window-manager";
import { registerWorkspaceIPCs } from "main/lib/workspace-ipcs";

Expand Down Expand Up @@ -46,15 +68,28 @@ app.on("open-url", (event, url) => {

await app.whenReady();

// Initialize desktop stores (migration, versioning) before registering IPCs
const { DesktopStores } = await import("main/lib/desktop-stores");
await DesktopStores.initialize();

// Register IPC handlers once at startup (not per-window)
registerWorkspaceIPCs();
registerPortIpcs();
registerDeepLinkIpcs();
registerUiIPCs();
const { registerWindowIPCs } = await import("main/lib/window-ipcs");
registerWindowIPCs();

await makeAppSetup(
() => windowManager.createWindow(),
() => windowManager.restoreWindows(),
);

// Stop all periodic rescans when app is quitting
app.on("before-quit", async () => {
const { workspaceRescanManager } = await import(
"main/lib/workspace-rescan"
);
workspaceRescanManager.stopAll();
});
})();
Loading
Loading