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
7 changes: 4 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
// Exclude common build and cache directories from the file explorer
"files.exclude": {
"**/.git": true,
Expand Down Expand Up @@ -102,9 +105,7 @@
"webfonts",
"wmii"
],
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
// Import settings
"javascript.preferences.importModuleSpecifier": "shortest",
"typescript.preferences.importModuleSpecifier": "non-relative"
}
27 changes: 20 additions & 7 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import nitpicker from "@alien_intelligence/eslint-plugin-nitpicker"
import tsParser from "@typescript-eslint/parser"

/**
* ESLint flat config, layers Nitpicker on top of Biome for nit-level enforcement,
* ESLint flat config, it exists only to host Nitpicker for nit-level enforcement,
* Biome still owns formatting and the broader lint rules.
*/
export default [
Expand All @@ -14,15 +14,28 @@ export default [
sourceType: "module",
},
plugins: { nitpicker },
rules: {
...nitpicker.configs.recommended.rules,
// TODO: re-enable once the upstream `e.charAt is not a function` crash is fixed,
// it currently trips on any TS `import type` line and aborts the whole lint run.
"nitpicker/no-british-english": "off",
},
rules: nitpicker.configs.recommended.rules,
},
{
files: ["src/**/*.ts", "src/**/*.tsx"],
...nitpicker.configs.breathing,
},
{
files: ["src/renderer/**/*.tsx", "src/renderer/**/*.ts"],
...nitpicker.configs.react,
},
{
files: ["src/renderer/**/*.tsx"],
ignores: ["src/renderer/components/ui/**"],
...nitpicker.configs.design,
rules: {
...nitpicker.configs.design.rules,
// The react95 "Button" is beveled window chrome, a flat list row or theme
// option is a "<button>" for the semantics alone and must not carry it
"nitpicker/no-raw-control-element": [
"warn",
{ allowIn: ["**/rows/FlatResults.tsx", "**/preferences/AppearanceTab.tsx"] },
],
},
},
]
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"styled-components": "^6.4.3"
},
"devDependencies": {
"@alien_intelligence/eslint-plugin-nitpicker": "^0.2.0",
"@alien_intelligence/eslint-plugin-nitpicker": "^0.10.1",
"@biomejs/biome": "^2.5.2",
"@tailwindcss/vite": "^4.3.1",
"@types/node": "^24.12.2",
Expand Down
1 change: 1 addition & 0 deletions src/main/config/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const DEFAULT_APP_CONFIG: AppConfig = {
preferences: {
theme: "original",
startupBehavior: "reopen-last",
codeEditorPath: "", // Empty means the installed editors are detected instead
autoLockTickIntervalMs: AUTO_LOCK_CONFIG.tickIntervalMs,
mcpEndpoint: MCP_CONFIG.endpoint,
mcpProbeIntervalMs: MCP_CONFIG.probeIntervalMs,
Expand Down
2 changes: 1 addition & 1 deletion src/main/config/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const WINDOWS_CONFIG: {
icon: appIcon,
webPreferences: WEB_PREFERENCES,
},
// Sized per variant by the dialogs service, from `DIALOGS_CONFIG`
// Sized per variant by the dialogs service, from "DIALOGS_CONFIG"
dialog: SECONDARY_WINDOW,
preferences: {
...SECONDARY_WINDOW,
Expand Down
10 changes: 9 additions & 1 deletion src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,28 @@ async function createMainWindow(): Promise<BrowserWindow> {

// Create the main application window when Electron is ready
app.whenReady().then(async () => {
// Handlers
// Handlers, the app shell itself
registerAppHandlers()
registerWindowsControlHandlers()
registerDialogsHandlers()

// Handlers reaching the repository
registerGitCommandsHandlers()
registerLfsCommandsHandlers()
registerFileTreeHandlers()

// Handlers reaching the project on disk
registerProjectsHandlers()
registerUProjectHandlers()
registerShellsHandlers()

// Handlers backing the long-running services
registerUpdaterHandlers()
registerMcpHandlers()
registerAutoLockHandlers()
registerCompileDbHandlers()

// Handlers owning the persisted state
registerViewStateHandlers()
registerPreferencesHandlers()

Expand Down
3 changes: 2 additions & 1 deletion src/main/lib/compileDb/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ function isInsideSourceTree(posixPath: string): boolean {
const segments = posixPath.split("/")

if (segments[0] === COMPILE_DB_CONFIG.sourceRootDirName) return true

if (segments[0] === COMPILE_DB_CONFIG.pluginsDirName) {
return segments.slice(2).includes(COMPILE_DB_CONFIG.sourceRootDirName)
}
Expand All @@ -62,7 +63,7 @@ export function classifySourcePath(relativePath: string): CompileDbFileKind | nu

const extension = path.posix.extname(posixPath).toLowerCase()

// A plugin's own descriptor sits above its `Source` tree rather than inside it,
// A plugin's own descriptor sits above its "Source" tree rather than inside it,
// and enabling a module in one moves the build with no source file moving
if (extension === COMPILE_DB_CONFIG.pluginDescriptorExtension) {
return isInsidePlugins(posixPath) ? "descriptor" : null
Expand Down
13 changes: 8 additions & 5 deletions src/main/lib/compileDb/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,9 @@ export class CompileDbService {
this._knownFiles = next
compileDbStore.set({ trackedFileCount: next.size })

// A failed run leaves the database out of date, and what fixes the usual
// cause (a header still half-written when it first appeared) is an edit
// rather than another file appearing, so a failure is worth retrying on any
// change at all rather than waiting for the set to move again
// A failed run leaves the database out of date, and its usual cause (a
// half-written header) is fixed by an edit rather than by a new file, so a
// failure is worth retrying on any change
if (!hasChanged && !this._hasFailed()) return

// A build or plugin descriptor can move the solution too, which is left to
Expand Down Expand Up @@ -288,7 +287,11 @@ export class CompileDbService {
if (unrealEngineRoot !== this._engineRootOverride) {
this._engineRootOverride = unrealEngineRoot
this._context = null
compileDbStore.set({ engineRoot: null, target: null, lastError: null })
compileDbStore.set({
engineRoot: null,
target: null,
lastError: null,
})
}

if (isCompileDbAutoRegenEnabled === this._isEnabled) return
Expand Down
6 changes: 3 additions & 3 deletions src/main/lib/compileDb/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ export async function watchSourceTrees(root: string, onChange: () => void): Prom
for (const watchRoot of resolveWatchRoots(root)) {
if (!(await pathExists(watchRoot))) continue

// A tree can still go away between that check and this call, and a watcher
// can die later on for the same reason, neither of which is worth taking
// the main process down over, the next project bind sets things up again
// A tree can go away between that check and this call, and a watcher can die
// later for the same reason, neither worth taking the main process down over
// since the next project bind sets things up again
try {
const watcher = watch(watchRoot, { recursive: true, persistent: false }, () => onChange())

Expand Down
101 changes: 90 additions & 11 deletions src/main/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const CONSTANTS = {
shellsOpenExternal: "shells:open-external",
shellsShowFolder: "shells:show-folder",
shellsOpenTerminal: "shells:open-terminal",
shellsOpenEditor: "shells:open-editor",
// Dialogs
dialogsConfirm: "dialogs:confirm",
dialogsMessage: "dialogs:message",
Expand Down Expand Up @@ -125,26 +126,104 @@ const CONSTANTS = {
missingFolderMessage: "The folder no longer exists on disk.",
openFolderFailureMessage: "The system could not open the folder in the file manager.",
noTerminalMessage: "No terminal could be launched, none of the ones we know about are installed or on PATH.",
noEditorMessage:
"No code editor could be found, none of the ones we know about are installed, set the path to yours under File > Options > General.",
openEditorFailureMessage: "The code editor set in the options is not where it is expected to be.",

/**
* How macOS is asked to open a folder with a given application, the editors are
* application bundles there rather than executables of their own.
*/
macOpen: {
command: "open",
appFlag: "-a",
},

/**
* The terminals tried in order when opening a folder.
*/
terminals: {
win32: [
{ command: "wt.exe", args: ["-d"], appendDir: true },
{ command: "cmd.exe", args: ["/c", "start", "", "cmd.exe"], appendDir: false },
{
command: "wt.exe",
args: ["-d"],
appendDir: true,
},
{
command: "cmd.exe",
args: ["/c", "start", "", "cmd.exe"],
appendDir: false,
},
],
darwin: [
{
command: "open",
args: ["-a", "Terminal"],
appendDir: true,
},
],
darwin: [{ command: "open", args: ["-a", "Terminal"], appendDir: true }],
linux: [
{ command: "x-terminal-emulator", args: [], appendDir: false },
{ command: "gnome-terminal", args: [], appendDir: false },
{ command: "konsole", args: [], appendDir: false },
{ command: "xfce4-terminal", args: [], appendDir: false },
{ command: "alacritty", args: [], appendDir: false },
{ command: "kitty", args: [], appendDir: false },
{ command: "xterm", args: [], appendDir: false },
{
command: "x-terminal-emulator",
args: [],
appendDir: false,
},
{
command: "gnome-terminal",
args: [],
appendDir: false,
},
{
command: "konsole",
args: [],
appendDir: false,
},
{
command: "xfce4-terminal",
args: [],
appendDir: false,
},
{
command: "alacritty",
args: [],
appendDir: false,
},
{
command: "kitty",
args: [],
appendDir: false,
},
{
command: "xterm",
args: [],
appendDir: false,
},
],
} as Record<string, { command: string; args: string[]; appendDir: boolean }[]>,

/**
* The code editors tried in order when opening a project folder.
*/
editors: {
win32: [
"%LOCALAPPDATA%\\Programs\\Microsoft VS Code\\Code.exe",
"%ProgramFiles%\\Microsoft VS Code\\Code.exe",
"%LOCALAPPDATA%\\Programs\\cursor\\Cursor.exe",
"%LOCALAPPDATA%\\Programs\\Microsoft VS Code Insiders\\Code - Insiders.exe",
"%LOCALAPPDATA%\\Programs\\Windsurf\\Windsurf.exe",
"%LOCALAPPDATA%\\Programs\\Rider\\bin\\rider64.exe",
"%ProgramFiles%\\Sublime Text\\sublime_text.exe",
],
darwin: [
"/Applications/Visual Studio Code.app",
"/Applications/Cursor.app",
"/Applications/Visual Studio Code - Insiders.app",
"/Applications/Windsurf.app",
"/Applications/Rider.app",
"/Applications/Sublime Text.app",
],
linux: ["code", "cursor", "code-insiders", "windsurf", "rider", "subl", "zed"],
} as Record<string, string[]>,
},
git: {
logFieldSeparator: "\x1f",
Expand All @@ -158,7 +237,7 @@ const CONSTANTS = {
contentDir: "Content",
assetExtension: ".uasset",
levelExtension: ".umap",
// The Unreal MCP exposes editor tools through a `call_tool` dispatcher rather
// The Unreal MCP exposes editor tools through a "call_tool" dispatcher rather
// than as first-class MCP tools, so each entry pairs the toolset owning it with
// its short name
dispatcher: "call_tool",
Expand Down
16 changes: 12 additions & 4 deletions src/main/lib/dialogs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class DialogRegistry {
}

/**
* Drops the entry for a closed window, cancelling any still-pending
* Drops the entry for a closed window, canceling any still-pending
* awaiter, called from the window's `closed` handler.
* @param id The window id.
*/
Expand Down Expand Up @@ -137,7 +137,7 @@ export function createDialogWindow(
* (a close counts as a cancel).
* @param parent The window that owns the dialog, or `null` to show it detached.
* @param options The dialog contents and variant.
* @returns `true` when confirmed, `false` when cancelled or closed.
* @returns `true` when confirmed, `false` when canceled or closed.
*/
export function openDialog(parent: BrowserWindow | null, options: DialogOptions): Promise<boolean> {
return new Promise(resolve => {
Expand All @@ -155,11 +155,19 @@ export function registerDialogsHandlers() {
)

ipcMain.on(CONSTANTS.ipc.dialogsMessage, (event, title: string, message: string) => {
openDialog(BrowserWindow.fromWebContents(event.sender), { variant: "message", title, message })
openDialog(BrowserWindow.fromWebContents(event.sender), {
variant: "message",
title,
message,
})
})

ipcMain.on(CONSTANTS.ipc.dialogsError, (event, title: string, message: string) => {
openDialog(BrowserWindow.fromWebContents(event.sender), { variant: "error", title, message })
openDialog(BrowserWindow.fromWebContents(event.sender), {
variant: "error",
title,
message,
})
})

ipcMain.on(CONSTANTS.ipc.dialogsErrorWithDetails, (event, title: string, message: string, details: string) => {
Expand Down
2 changes: 1 addition & 1 deletion src/main/lib/gitCommands/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export async function runGit(args: string[], options: RunGitOptions): Promise<Gi
signal?: NodeJS.Signals | null
}

// A missing `git` binary surfaces as a spawn error with a string code (e.g "ENOENT")
// A missing "git" binary surfaces as a spawn error with a string code (e.g "ENOENT")
if (typeof typedError.code === "string") {
throw new Error(`Failed to run git (${typedError.code}). Is git installed and on your PATH?`)
}
Expand Down
1 change: 1 addition & 0 deletions src/main/lib/gitCommands/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ export function parseStatus(output: string): GitStatus {
}

status.isClean = status.changes.length === 0

return status
}

Expand Down
12 changes: 10 additions & 2 deletions src/main/lib/lfsCommands/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ export function registerLfsCommandsHandlers() {
safeHandle(CONSTANTS.ipc.lfsCommandsLockPaths, (event, dir: string, paths: string[], requestId: string | null) =>
lockPaths(dir, paths, (done, total) => {
if (!requestId || event.sender.isDestroyed()) return
const payload: LfsLockProgress = { requestId, done, total }
const payload: LfsLockProgress = {
requestId,
done,
total,
}
event.sender.send(CONSTANTS.ipc.lfsCommandsLockProgress, payload)
}),
)
Expand All @@ -39,7 +43,11 @@ export function registerLfsCommandsHandlers() {
(event, dir: string, paths: string[], force: boolean | undefined, requestId: string | null) =>
unlockPaths(dir, paths, force, (done, total) => {
if (!requestId || event.sender.isDestroyed()) return
const payload: LfsLockProgress = { requestId, done, total }
const payload: LfsLockProgress = {
requestId,
done,
total,
}
event.sender.send(CONSTANTS.ipc.lfsCommandsLockProgress, payload)
}),
)
Expand Down
Loading
Loading