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 .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ patches

# Design and plan documents: their code blocks are partial snippets, not code.
docs/superpowers

# Git worktrees of other branches (each formats and lints itself).
.claude/worktrees
Binary file modified docs/mockups/cli-settings-skill.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/mockups/generate-empty/after.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/mockups/generate-empty/before.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/mockups/generate-empty/settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/mockups/terminal-v5/shots/impl-10-s2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/mockups/terminal-v5/shots/impl-collapsed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/mockups/terminal-v5/shots/impl-completed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/mockups/terminal-v5/shots/impl-empty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/mockups/terminal-v5/shots/impl-generate.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/mockups/terminal-v5/shots/impl-info.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/mockups/terminal-v5/shots/impl-menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/mockups/terminal-v5/shots/impl-preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/mockups/terminal-v5/shots/impl-settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/mockups/terminal-v5/shots/impl-tools.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/mockups/view-zoom/shots/impl-menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/mockups/view-zoom/shots/impl-size0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/mockups/view-zoom/shots/impl-size1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/mockups/view-zoom/shots/impl-size2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/mockups/view-zoom/shots/impl-size3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/mockups/view-zoom/shots/impl-size4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/mockups/view-zoom/shots/impl-size5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 4 additions & 3 deletions e2e/cli-packed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ test('Ctrl+C in a console cancels cleanly: exit 130, no ffmpeg, no partial outpu
])
const result = join(work, 'result.json')
const before = ffmpegPids()
// Start-Process gives the helper a console of its own (minimized). It joins
// Start-Process gives the helper a console of its own, hidden so the run never
// shows a window or takes focus (console input/output work the same). It joins
// -ArgumentList with spaces, so every value carries its own quotes.
const q = (s: string): string => `'"${s}"'`
const helperArgs = [
Expand All @@ -125,9 +126,9 @@ test('Ctrl+C in a console cancels cleanly: exit 130, no ffmpeg, no partial outpu
[
'-NoProfile',
'-Command',
`Start-Process powershell.exe -Wait -WindowStyle Minimized -ArgumentList @(${helperArgs})`
`Start-Process powershell.exe -Wait -WindowStyle Hidden -ArgumentList @(${helperArgs})`
],
{ encoding: 'utf-8', timeout: 170_000 }
{ encoding: 'utf-8', timeout: 170_000, windowsHide: true }
)
expect(r.status, r.stderr).toBe(0)
const out = JSON.parse(readFileSync(result, 'utf-8').replace(/^\uFEFF/, '')) as {
Expand Down
8 changes: 2 additions & 6 deletions e2e/cli.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { test, expect } from '@playwright/test'
import { _electron } from 'playwright'
import { execFileSync, spawn, spawnSync } from 'child_process'
import { createHash } from 'crypto'
import {
Expand All @@ -14,7 +13,7 @@ import {
} from 'fs'
import { tmpdir } from 'os'
import { dirname, join } from 'path'
import { FFMPEG, MAGICK, MUTOOL, ROOT, magickEnv } from './helpers'
import { FFMPEG, MAGICK, MUTOOL, ROOT, magickEnv, launchApp } from './helpers'

// The CLI as a real process (spec 8.2): `node out/main/cli.js` against the
// repo's bundled tools, with an isolated userData. Run `npm run build` first.
Expand Down Expand Up @@ -306,10 +305,7 @@ test('a CLI killed outright leaves no partial output and no ffmpeg (watchdog)',

test('works while the app is open, and its jobs never reach the app', async () => {
const ud = join(work, '.ud')
const app = await _electron.launch({
args: [ROOT],
env: { ...process.env, FILESMITH_USER_DATA: ud }
})
const app = await launchApp({ FILESMITH_USER_DATA: ud })
try {
const page = await app.firstWindow()
image('cliprobe.png')
Expand Down
49 changes: 49 additions & 0 deletions e2e/generate-empty.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { test, expect } from '@playwright/test'
import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { MAIN, ROOT, launchApp } from './helpers'

// Generate with nothing set up: a fresh userData and a throwaway home folder
// (homedir() follows USERPROFILE, so ComfyUI discovery under the real profile
// finds nothing). The tab must explain itself instead of showing an empty
// model control and a silently disabled Generate.
const SHOTS = join(ROOT, 'docs', 'mockups', 'generate-empty')

test('Generate without a model shows one next step and says why it cannot run', async () => {
test.skip(!existsSync(MAIN), 'run `npm run build` first')
const profile = mkdtempSync(join(tmpdir(), 'fs-profile-'))
mkdirSync(join(profile, 'Downloads'))
const userData = mkdtempSync(join(tmpdir(), 'fs-ud-'))
const app = await launchApp({ USERPROFILE: profile, FILESMITH_USER_DATA: userData })
try {
const page = await app.firstWindow()
await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].setSize(1440, 900))
const nav = page.getByRole('navigation', { name: 'Operations' })
await nav.getByRole('button', { name: 'Generate' }).click()
const insp = page.getByRole('complementary', { name: 'Inspector' })
await expect(insp.getByText('No image model yet')).toBeVisible()
await expect(page.getByTestId('run')).toBeDisabled()
await expect(insp.getByText('Choose your ComfyUI folder to generate')).toBeVisible()
// A prompt alone does not turn it on: there is still nothing to run with.
const prompt = page.getByRole('textbox', { name: 'Prompt', exact: true })
await prompt.fill('a lighthouse at dusk')
await expect(page.getByTestId('run')).toBeDisabled()
await prompt.fill('')
// No toolbar row and no Generate button there: Run lives in the inspector
// footer only, and the prompt sits flush at the top of the centre column.
await expect(page.getByRole('toolbar', { name: 'Generate actions' })).toHaveCount(0)
if (process.env['FILESMITH_SHOTS']) {
mkdirSync(SHOTS, { recursive: true })
await page.screenshot({ path: join(SHOTS, 'after.png') })
await nav.getByRole('button', { name: 'Settings' }).click()
await expect(page.getByText('Not set')).toBeVisible()
await expect(page.getByRole('button', { name: 'Choose folder' })).toBeVisible()
await page.screenshot({ path: join(SHOTS, 'settings.png') })
}
} finally {
await app.close()
rmSync(profile, { recursive: true, force: true })
rmSync(userData, { recursive: true, force: true })
}
})
14 changes: 13 additions & 1 deletion e2e/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Page } from 'playwright'
import { _electron, type ElectronApplication, type Page } from 'playwright'
import { join, resolve } from 'path'
import type { JobEvent } from '../src/shared/types'

Expand All @@ -19,6 +19,18 @@ export const magickEnv = {
MAGICK_CONFIGURE_PATH: BIN
}

/** Launch the built app for a spec. Every spec goes through here so the window
* stays hidden (FILESMITH_E2E_HIDDEN): it paints for screenshots but is never
* shown or focused, so a test run never steals focus from the desktop. */
export function launchApp(
env: Record<string, string | undefined> = {}
): Promise<ElectronApplication> {
return _electron.launch({
args: [ROOT],
env: { ...process.env, ...env, FILESMITH_E2E_HIDDEN: '1' } as Record<string, string>
})
}

/** Run one job through the real preload bridge; resolve on its terminal event. */
export function runJob(
page: Page,
Expand Down
9 changes: 3 additions & 6 deletions e2e/rowmenu.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { _electron, type ElectronApplication, type Page } from 'playwright'
import { type ElectronApplication, type Page } from 'playwright'
import { test, expect } from '@playwright/test'
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { MAIN, ROOT } from './helpers'
import { MAIN, launchApp } from './helpers'

// The files table's right-click menu and the one-file inspector panes, on a
// seeded session. Nothing here deletes a file: Delete is only opened and
Expand Down Expand Up @@ -39,10 +39,7 @@ test.beforeAll(async () => {
queues: { convert: { items } }
}
writeFileSync(join(userData, 'session.json'), JSON.stringify(session))
app = await _electron.launch({
args: [ROOT],
env: { ...process.env, FILESMITH_USER_DATA: userData }
})
app = await launchApp({ FILESMITH_USER_DATA: userData })
page = await app.firstWindow()
await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].setSize(1440, 900))
await expect(row('alpha')).toBeVisible()
Expand Down
8 changes: 2 additions & 6 deletions e2e/skill.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { _electron } from 'playwright'
import { test, expect } from '@playwright/test'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { MAIN, ROOT } from './helpers'
import { MAIN, ROOT, launchApp } from './helpers'

// The Settings button (M10) against a throwaway home folder: homedir() follows
// USERPROFILE on Windows, so the real ~/.claude is never touched.
Expand All @@ -17,10 +16,7 @@ test('Settings installs the Claude skill and shows its version', async () => {
const version = (
JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8')) as { version: string }
).version
const app = await _electron.launch({
args: [ROOT],
env: { ...process.env, USERPROFILE: profile, FILESMITH_USER_DATA: userData }
})
const app = await launchApp({ USERPROFILE: profile, FILESMITH_USER_DATA: userData })
try {
const page = await app.firstWindow()
await page
Expand Down
5 changes: 3 additions & 2 deletions e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { _electron, type ElectronApplication, type Page } from 'playwright'
import { type ElectronApplication, type Page } from 'playwright'
import { launchApp } from './helpers'
import { test, expect } from '@playwright/test'
import { execFileSync } from 'child_process'
import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'fs'
Expand Down Expand Up @@ -38,7 +39,7 @@ test.beforeAll(async () => {
// makes app.getAppPath() resolve to out/main, which silently hides every
// resources/ tree and lets resolveTool fall back to PATH binaries — the
// suite would then test the machine's tools, not the bundled ones.
app = await _electron.launch({ args: [ROOT] })
app = await launchApp()
page = await app.firstWindow()
})

Expand Down
9 changes: 3 additions & 6 deletions e2e/ui.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { _electron, type ElectronApplication, type Locator, type Page } from 'playwright'
import { type ElectronApplication, type Locator, type Page } from 'playwright'
import { test, expect } from '@playwright/test'
import { existsSync, mkdtempSync, rmSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { MAIN, ROOT } from './helpers'
import { MAIN, launchApp } from './helpers'

// UI behaviour the unit suite cannot reach: the sidebar, the inspector tabs and
// the empty workspace. A private userData keeps the user's real session and
Expand All @@ -15,10 +15,7 @@ let userData: string
test.beforeAll(async () => {
test.skip(!existsSync(MAIN), 'run `npm run build` first')
userData = mkdtempSync(join(tmpdir(), 'filesmith-ui-'))
app = await _electron.launch({
args: [ROOT],
env: { ...process.env, FILESMITH_USER_DATA: userData }
})
app = await launchApp({ FILESMITH_USER_DATA: userData })
page = await app.firstWindow()
await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].setSize(1440, 900))
})
Expand Down
9 changes: 3 additions & 6 deletions e2e/viewsizes.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { _electron, type ElectronApplication, type Page } from 'playwright'
import { type ElectronApplication, type Page } from 'playwright'
import { test, expect } from '@playwright/test'
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { execFileSync } from 'child_process'
import { FFMPEG, MAGICK, MAIN, ROOT, magickEnv } from './helpers'
import { FFMPEG, MAGICK, MAIN, magickEnv, launchApp } from './helpers'

// View sizes (spec docs/superpowers/specs/2026-10-05-view-sizes-design.md) on a
// seeded session: four images and a video, so there are two group headers.
Expand Down Expand Up @@ -44,10 +44,7 @@ test.beforeAll(async () => {
queues: { convert: { items } }
}
writeFileSync(join(userData, 'session.json'), JSON.stringify(session))
app = await _electron.launch({
args: [ROOT],
env: { ...process.env, FILESMITH_USER_DATA: userData }
})
app = await launchApp({ FILESMITH_USER_DATA: userData })
page = await app.firstWindow()
await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].setSize(1440, 900))
await expect(row('alpha')).toBeVisible()
Expand Down
9 changes: 3 additions & 6 deletions e2e/visual.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { _electron, type ElectronApplication, type Page } from 'playwright'
import { type ElectronApplication, type Page } from 'playwright'
import { test, expect } from '@playwright/test'
import { execFileSync } from 'child_process'
import { existsSync, mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { MAGICK, MAIN, ROOT, magickEnv } from './helpers'
import { MAGICK, MAIN, ROOT, magickEnv, launchApp } from './helpers'

// Screenshots for the owner's side-by-side with the signed-off mockup. No pixel
// assertions (spec 7.3): the shots are evidence, not a gate.
Expand Down Expand Up @@ -93,10 +93,7 @@ test.beforeAll(async () => {
}
}
writeFileSync(join(userData, 'session.json'), JSON.stringify(session))
app = await _electron.launch({
args: [ROOT],
env: { ...process.env, FILESMITH_USER_DATA: userData }
})
app = await launchApp({ FILESMITH_USER_DATA: userData })
page = await app.firstWindow()
await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].setSize(1440, 900))
await expect(
Expand Down
16 changes: 13 additions & 3 deletions e2e/workflows.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { _electron, type ElectronApplication, type Page } from 'playwright'
import { type ElectronApplication, type Page } from 'playwright'
import { test, expect } from '@playwright/test'
import { execFileSync } from 'child_process'
import {
Expand All @@ -12,7 +12,17 @@ import {
} from 'fs'
import { tmpdir } from 'os'
import { basename, join } from 'path'
import { FFMPEG, FFPROBE, MAGICK, MAIN, MUTOOL, ROOT, SEVENZIP, magickEnv, runJob } from './helpers'
import {
FFMPEG,
FFPROBE,
MAGICK,
MAIN,
MUTOOL,
SEVENZIP,
magickEnv,
runJob,
launchApp
} from './helpers'

// The full workflow matrix, run against the REAL built app: every operation of
// every category, through the actual preload bridge -> IPC -> queue -> engine
Expand Down Expand Up @@ -157,7 +167,7 @@ test.beforeAll(async () => {
// makes app.getAppPath() resolve to out/main, which silently hides every
// resources/ tree and lets resolveTool fall back to PATH binaries — the
// suite would then test the machine's tools, not the bundled ones.
app = await _electron.launch({ args: [ROOT] })
app = await launchApp()
page = await app.firstWindow()
})

Expand Down
11 changes: 10 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,16 @@ export default tseslint.config(
// build/installer is the NSIS setup kit (ported from Prism): CommonJS
// generator scripts + committed media that follow that kit's conventions,
// not the app toolchain's.
ignores: ['out', 'dist', 'node_modules', 'resources', '.harness', 'build/installer', 'patches']
ignores: [
'out',
'dist',
'node_modules',
'resources',
'.harness',
'build/installer',
'patches',
'.claude/worktrees'
]
},
js.configs.recommended,
...tseslint.configs.recommended,
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "filesmith",
"version": "0.7.0",
"version": "0.7.1",
"description": "A desktop file toolkit: convert, compress, resize, upscale, remove backgrounds, and PDF tools.",
"author": "Max",
"license": "MIT",
Expand Down
12 changes: 9 additions & 3 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import { bootEngine } from './boot'
const userDataOverride = process.env['FILESMITH_USER_DATA']
if (userDataOverride) app.setPath('userData', userDataOverride)

// Test hook: e2e runs the window hidden so a test run never shows a window or
// steals focus. It still paints (screenshots work) and is never shown/focused.
const e2eHidden = process.env['FILESMITH_E2E_HIDDEN'] === '1'

// The engine's view of its host (spec M1). Read after the e2e userData override
// so tests that seed a session still get their temp folder.
setEngineEnv({
Expand Down Expand Up @@ -137,13 +141,15 @@ function createWindow(): void {
// Frameless: the renderer draws the 32px title bar and window controls.
frame: false,
backgroundColor: '#0a0a0a',
...(e2eHidden ? { paintWhenInitiallyHidden: true, skipTaskbar: true } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
sandbox: false,
...(e2eHidden ? { backgroundThrottling: false } : {})
}
})

mainWindow.on('ready-to-show', () => mainWindow.show())
if (!e2eHidden) mainWindow.on('ready-to-show', () => mainWindow.show())

// Open external links in the OS browser, never in-app.
mainWindow.webContents.setWindowOpenHandler((details) => {
Expand Down Expand Up @@ -182,7 +188,7 @@ if (!app.requestSingleInstanceLock()) {
} else {
app.on('second-instance', () => {
const win = BrowserWindow.getAllWindows()[0]
if (win) {
if (win && !e2eHidden) {
if (win.isMinimized()) win.restore()
win.focus()
}
Expand Down
Loading
Loading