Adds a better diff design and removes the select and copy feature

This commit is contained in:
2026-09-10 08:36:42 +02:00
parent bb012702fe
commit 008adbfdb6
19 changed files with 582 additions and 2756 deletions
+2 -2
View File
@@ -27,7 +27,7 @@ export interface TerminalTheme {
export interface HelderConfig {
ai: { command: string; autoLaunch: boolean }
editor: { autoSave: boolean; tabSize: number; wordWrap: WordWrap; copyOnSelect: boolean }
editor: { autoSave: boolean; tabSize: number; wordWrap: WordWrap }
git: { confirmDiscard: boolean; confirmStage: boolean; confirmUnstage: boolean; defaultDiffMode: DiffMode; refreshInterval: number }
files: { exclude: string[]; followGitignore: boolean }
terminal: {
@@ -54,7 +54,7 @@ export interface HelderConfig {
export const DEFAULTS: HelderConfig = {
ai: { command: 'claude', autoLaunch: true },
editor: { autoSave: false, tabSize: 4, wordWrap: 'markdown', copyOnSelect: true },
editor: { autoSave: false, tabSize: 4, wordWrap: 'markdown' },
git: { confirmDiscard: true, confirmStage: false, confirmUnstage: false, defaultDiffMode: 'diff', refreshInterval: 10000 },
files: { exclude: [], followGitignore: false },
terminal: {
+14 -1
View File
@@ -1,3 +1,4 @@
import { mkdirSync } from 'node:fs'
import { app, crashReporter, dialog, shell, BrowserWindow, type WebContents } from 'electron'
import { formatErr, getLogDir, getLogPath, initLogger, log, logger } from './logger'
@@ -26,10 +27,22 @@ let fatalDialogOpen = false
* native crashes, and the log file should exist before anything can fail. */
export function initDiagnostics(isDev: boolean): void {
// app.getPath('logs') is ~/Library/Logs/<name> on macOS, so the name must be
// set before we ask for the path or the folder is called "Electron".
// set first or the folder is called "Electron". The name later becomes the
// project's own, hence the paths are read here and pinned to "Helder".
app.setName('Helder')
let pinErr: unknown = null
try {
for (const name of ['userData', 'logs'] as const) {
const dir = app.getPath(name)
mkdirSync(dir, { recursive: true }) // setPath throws on a directory that does not exist yet
app.setPath(name, dir)
}
} catch (e) {
pinErr = e // startup must survive this; the logger does not exist yet
}
initLogger({ dir: app.getPath('logs'), mirror: isDev })
if (pinErr) logger.warn('session', 'could not pin the app paths — a rename may move them', { err: formatErr(pinErr) })
// Native minidumps for crashes no JS handler can see. Local only — nothing is
// uploaded anywhere (there is no server, and this is a personal tool).
+11 -8
View File
@@ -3,7 +3,7 @@ import { readFileSync, statSync } from 'node:fs'
import { spawn } from 'node:child_process'
import { app, dialog, shell, BrowserWindow, ipcMain, Menu } from 'electron'
import { watch, type FSWatcher } from 'chokidar'
import { addRecentProject, getName, getRecentProjects, getRoot, openDialog, setRoot } from './project'
import { addRecentProject, getName, getProjectTitle, getRecentProjects, getRoot, openDialog, setRoot } from './project'
import { createProjectDir, createProjectFile, deleteProjectFile, readAll, readDirChildren, readImageDataUrl, readProjectFile, readTree, renameProjectEntry, writeProjectFile } from './fs-service'
import { commit, discard, load, push, stage, unstage } from './git-service'
import { createPty, killAllPtys, killPty, ptyAvailable, resizePty, writePty } from './pty-service'
@@ -200,12 +200,12 @@ function buildAppMenu(): Menu {
/**
* macOS reads the Dock tile's name from the bundle's CFBundleName at launch, so
* every Helder process shows the same "Helder" tooltip — `app.setName()` moves
* the menu-bar name and the paths, but never the Dock label. The open project is
* the menu-bar name, but never the Dock label. The open project is
* therefore named in the Dock *menu* instead: a disabled first item, so a
* right-click tells the two tiles apart.
*/
function buildDockMenu(): Menu {
const name = getName()
const name = getRoot() ? getProjectTitle() : ''
return Menu.buildFromTemplate([
...(name ? [{ label: name, enabled: false }, { type: 'separator' as const }] : []),
{ label: 'New Window', click: () => spawnInstance() },
@@ -357,11 +357,13 @@ function registerIpc(): void {
})
}
/** The window title and the Dock menu both carry the open project's folder name
* (the title falls back to the app name when nothing is open). Push both after a
* project change. */
/** Window title, app name and Dock menu all carry the open project's name. The
* menu is rebuilt because the appMenu role items (About, Hide, Quit) read
* `app.name` once, at build time. */
function syncProjectChrome(): void {
const title = getName() || 'Helder'
const title = getProjectTitle()
app.setName(title)
Menu.setApplicationMenu(buildAppMenu())
for (const w of BrowserWindow.getAllWindows()) w.setTitle(title)
app.dock?.setMenu(buildDockMenu())
}
@@ -374,7 +376,7 @@ function createWindow(): void {
minHeight: 680,
show: false,
backgroundColor: '#16171a',
title: getName() || 'Helder',
title: getProjectTitle(),
titleBarStyle: isMac ? 'hiddenInset' : 'default',
trafficLightPosition: isMac ? { x: 14, y: 12 } : undefined,
webPreferences: {
@@ -424,6 +426,7 @@ app.whenReady().then(async () => {
logger.info('session', 'ready', { root: initialRoot, logPath: getLogPath() })
if (initialRoot) {
await resolveConfig(initialRoot)
syncProjectChrome()
await addRecentProject(initialRoot)
startWatcher()
startConfigWatcher()
+27 -1
View File
@@ -1,7 +1,8 @@
import { basename, join, resolve } from 'node:path'
import { existsSync, statSync } from 'node:fs'
import { existsSync, readFileSync, statSync } from 'node:fs'
import { readFile, writeFile } from 'node:fs/promises'
import { app, dialog, BrowserWindow } from 'electron'
import { formatErr, logger } from './logger'
/**
* One project per window. The root is resolved (in order) from $HELDER_PROJECT or
@@ -34,6 +35,31 @@ export function getName(): string {
return root ? basename(root) || root : ''
}
/**
* The name this process shows in the macOS menu bar: `APP_NAME` from the
* project's .env, else the folder name. Kept apart from `getName()`, which stays
* the folder name for recents and the renderer.
*/
export function getProjectTitle(): string {
if (!root) return 'Helder'
try {
let raw: string | null = null
for (const line of readFileSync(join(root, '.env'), 'utf8').split(/\r?\n/)) {
if (line.trimStart().startsWith('#')) continue
const m = /^\s*APP_NAME\s*=(.*)$/.exec(line)
if (m) raw = m[1]
}
const value = (raw ?? '').trim().replace(/^(['"])([\s\S]*)\1$/, '$2').trim()
if (value) return value
} catch (e) {
// A project without a .env is the normal case, not a fault.
if ((e as NodeJS.ErrnoException)?.code !== 'ENOENT') {
logger.warn('project', '.env unreadable — using the folder name', { err: formatErr(e) })
}
}
return getName()
}
/** Point the window at a project root (absolute) and remember it in recents. */
export function setRoot(next: string): void {
root = resolve(next)