commit
This commit is contained in:
+4
-2
@@ -11,10 +11,12 @@ import { join } from 'node:path'
|
||||
* Effective value = config.json over config.default.json, merged key by key.
|
||||
*/
|
||||
export type DiffMode = 'original' | 'updated' | 'diff'
|
||||
/** Soft wrap of long lines: never, always, or only in Markdown files. */
|
||||
export type WordWrap = 'off' | 'on' | 'markdown'
|
||||
|
||||
export interface HelderConfig {
|
||||
ai: { command: string; autoLaunch: boolean }
|
||||
editor: { autoSave: boolean; tabSize: number }
|
||||
editor: { autoSave: boolean; tabSize: number; wordWrap: WordWrap }
|
||||
git: { confirmDiscard: boolean; confirmStage: boolean; confirmUnstage: boolean; defaultDiffMode: DiffMode; refreshInterval: number }
|
||||
files: { exclude: string[]; followGitignore: boolean }
|
||||
terminal: { shell: string | null }
|
||||
@@ -23,7 +25,7 @@ export interface HelderConfig {
|
||||
|
||||
export const DEFAULTS: HelderConfig = {
|
||||
ai: { command: 'claude', autoLaunch: true },
|
||||
editor: { autoSave: false, tabSize: 4 },
|
||||
editor: { autoSave: false, tabSize: 4, wordWrap: 'markdown' },
|
||||
git: { confirmDiscard: true, confirmStage: false, confirmUnstage: false, defaultDiffMode: 'diff', refreshInterval: 10000 },
|
||||
files: { exclude: [], followGitignore: false },
|
||||
terminal: { shell: null },
|
||||
|
||||
@@ -106,12 +106,22 @@ function installAppHooks(): void {
|
||||
contents.on('preload-error', (_ev, preloadPath, error) => {
|
||||
logger.error('preload', 'preload script threw — window.helder will be undefined (mock-data fallback)', error, { preloadPath })
|
||||
})
|
||||
let sawRoLoop = false
|
||||
contents.on('console-message', (...a: unknown[]) => {
|
||||
// Electron ≥36 passes a single event object; older versions pass
|
||||
// (event, level, message, line, sourceId). Support both so a version bump
|
||||
// doesn't quietly stop capturing renderer console output.
|
||||
const d = normaliseConsoleMessage(a)
|
||||
if (!d || d.level < 2) return // warnings + errors only; skip log/info noise
|
||||
// "ResizeObserver loop …" is a browser layout notice, not a fault, and it
|
||||
// fires once per frame — a window drag would bury everything else. The
|
||||
// renderer suppresses its own repeats the same way (see log.ts).
|
||||
if (d.message.startsWith('ResizeObserver loop')) {
|
||||
if (sawRoLoop) return
|
||||
sawRoLoop = true
|
||||
log('warn', 'console', d.message + ' (browser layout notice; repeats suppressed)', { source: d.source, line: d.line })
|
||||
return
|
||||
}
|
||||
log(d.level >= 3 ? 'error' : 'warn', 'console', d.message, { source: d.source, line: d.line })
|
||||
})
|
||||
})
|
||||
|
||||
+24
-1
@@ -1,4 +1,4 @@
|
||||
import { mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
||||
import { mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'
|
||||
import { dirname, join, relative, sep } from 'node:path'
|
||||
import { listFiles, rgAvailable } from './search-service'
|
||||
|
||||
@@ -261,6 +261,29 @@ export async function createProjectDir(root: string, rel: string): Promise<void>
|
||||
await mkdir(target, { recursive: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a project file or folder. `rel` is the current relative path, `name`
|
||||
* the new basename (no slashes — a rename stays in the same folder). Returns the
|
||||
* new relative path. Refuses to escape the project root and throws if the target
|
||||
* name is already taken, so a rename never clobbers an existing file.
|
||||
*/
|
||||
export async function renameProjectEntry(root: string, rel: string, name: string): Promise<string> {
|
||||
const clean = name.trim().replace(/\/+$/, '')
|
||||
if (!clean || clean.includes('/') || clean === '.' || clean === '..') throw new Error('invalid name')
|
||||
const from = join(root, rel)
|
||||
const parent = rel.includes('/') ? rel.slice(0, rel.lastIndexOf('/')) : ''
|
||||
const next = parent ? `${parent}/${clean}` : clean
|
||||
const to = join(root, next)
|
||||
if (relative(root, from).startsWith('..') || relative(root, to).startsWith('..')) throw new Error('outside project root')
|
||||
if (from === to) return rel
|
||||
// Case-only renames (foo.md → Foo.md) hit an existing path on macOS' case
|
||||
// insensitive filesystem, so only guard when the name really differs.
|
||||
const sameName = from.toLowerCase() === to.toLowerCase()
|
||||
if (!sameName && await stat(to).catch(() => null)) throw new Error('name already exists')
|
||||
await rename(from, to)
|
||||
return next
|
||||
}
|
||||
|
||||
/** Delete a project file or folder (relative path). Stays inside the project root. */
|
||||
export async function deleteProjectFile(root: string, rel: string): Promise<void> {
|
||||
const target = join(root, rel)
|
||||
|
||||
+2
-1
@@ -4,7 +4,7 @@ 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 { createProjectDir, createProjectFile, deleteProjectFile, readAll, readDirChildren, readImageDataUrl, readProjectFile, readTree, writeProjectFile } from './fs-service'
|
||||
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'
|
||||
import { getConfig, getRecent, getThemeCss, resolveConfig, setRecent } from './config'
|
||||
@@ -284,6 +284,7 @@ function registerIpc(): void {
|
||||
handle('fs:delete', (_e, rel: string) => { const r = getRoot(); if (r) return deleteProjectFile(r, rel) })
|
||||
handle('fs:create', (_e, rel: string) => { const r = getRoot(); if (r) return createProjectFile(r, rel) })
|
||||
handle('fs:mkdir', (_e, rel: string) => { const r = getRoot(); if (r) return createProjectDir(r, rel) })
|
||||
handle('fs:rename', (_e, rel: string, name: string) => { const r = getRoot(); return r ? renameProjectEntry(r, rel, name) : rel })
|
||||
// Scratch note: <project>/.notes.txt, saved when the window loses focus.
|
||||
handle('notes:read', () => { const r = getRoot(); return r ? readNote(r) : '' })
|
||||
handle('notes:write', (_e, text: string) => { const r = getRoot(); if (r) return writeNote(r, text) })
|
||||
|
||||
Reference in New Issue
Block a user