Skip to content
Open
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
55 changes: 55 additions & 0 deletions .agents/friction-log/20260919110650-yarn-start-exits/friction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
title: '`yarn start` exits immediately without a TTY, so the agent-browser workflow in CLAUDE.md loads a blank window'
severity: 'minor'
---

## Expected Behavior

`CLAUDE.md` tells an agent to "Start the app with `yarn start`, then drive the
renderer with agent-browser" over CDP on port 9222. That should leave a running
app with its Vite dev server still up.

## Current Behavior

`electron-forge start` prints "Type rs in terminal to restart main process",
then exits about 3.5 seconds later with `✘ [ERROR] The build was canceled` and
`Done in 3.65s` whenever stdin is not a TTY — which is every non-interactive
shell an agent has. Electron survives as an orphan and keeps answering CDP, so
the failure does not look like one: `curl 127.0.0.1:9222/json/list` returns a
page and `agent-browser --cdp 9222 snapshot` answers `(empty page)`.

The Vite dev server died with its parent, so the window has nothing to load:

```
(node:94123) electron: Failed to load URL: http://localhost:5173/ with error: ERR_CONNECTION_REFUSED
```

The tell is the CDP target's title — `localhost:5173` rather than `Squeal`.

## Possible Solution

Hold stdin open for it:

```bash
tail -f /dev/null | yarn start
```

Worth a line in the agent-browser section of `CLAUDE.md`, or a `yarn start:ci`
script that does it.

## Minimal Reproducible Example

```bash
nohup yarn start > start.log 2>&1 &
# wait ~5s
tail -3 start.log # "Done in 3.65s"
curl -s 127.0.0.1:9222/json/list | grep title # "localhost:5173", not "Squeal"
```

## Context

Hit while smoke-testing the oxfmt/oxlint/TypeScript 7 migration against the
running app. Cost about fifteen minutes, most of it spent suspecting the change
under test: that migration touched the window's load path in `src/main.ts`, and
a blank window with `ERR_CONNECTION_REFUSED` is exactly what a regression there
would look like.
20 changes: 0 additions & 20 deletions .eslintrc.json

This file was deleted.

1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
.dynamodb/
.env
.env.test
.eslintcache
.fusebox/
.lock-wscript
.next
Expand Down
27 changes: 27 additions & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"arrowParens": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"proseWrap": "always",
"semi": false,
"singleAttributePerLine": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"printWidth": 80,
"sortPackageJson": false,
"ignorePatterns": [
// `frog log` writes these; its Markdown is not prose-wrapped at 80 columns
// and its YAML `title:` stays on one line, so checking them turns the
// Format job red on the next push after logging friction.
".agents/friction-log/",
".cache/",
"CHANGELOG.md",
"build/",
"coverage/",
"design/",
"dist/",
"node_modules/"
]
}
23 changes: 23 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["import", "oxc", "typescript", "unicorn"],
"categories": {
"correctness": "error"
},
"rules": {
"typescript/no-explicit-any": "error"
},
"overrides": [
{
// `expect(client.close).toHaveBeenCalled()` is the assertion vitest asks
// for, and it reads the method without calling it every single time — so
// in test files this rule reports the framework rather than a defect. It
// stays on everywhere else, where it found a real one in
// `probeServerVersion`.
"files": ["**/*.test.ts", "**/*.test.tsx", "vitest.setup.ts"],
"rules": {
"typescript/unbound-method": "off"
}
}
]
}
11 changes: 0 additions & 11 deletions .prettierignore

This file was deleted.

11 changes: 0 additions & 11 deletions .prettierrc.json

This file was deleted.

31 changes: 29 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ disposes on `before-quit`.

- `yarn start` - Development mode
- `yarn seed` - Seed PostgreSQL with Pagila sample data
- `yarn lint` / `yarn format` - Code quality (`yarn format:check` is the
read-only form CI runs)
- `yarn lint` / `yarn format` - oxlint (`.oxlintrc.json`) and oxfmt
(`.oxfmtrc.json`); `yarn format:check` is the read-only form CI runs
- `yarn typecheck` - Runs two projects: `tsconfig.backend.json` (strict, covers
`src/server` and `src/glue`) and `tsconfig.renderer.json`
- `yarn test` - Vitest, split by environment: a `backend` project (node) holding
Expand Down Expand Up @@ -164,6 +164,26 @@ from older databases. A new table or column goes in both
the Edit menu is what supplies ⌘C/⌘V/⌘X/⌘A/⌘Z. The template is the default
minus `reload` and `forceReload`, and `menu.test.ts` fails if either comes
back.
- Typechecking is TypeScript 7 (the native `tsc`). `baseUrl` was removed in 7,
so `tsconfig.base.json` has only `paths`, which resolve relative to the config
file that declares them. The `typescript` package now ships the `tsc` binary
and nothing else — no `tsserver.js`, no compiler API — so an editor pointed at
the workspace TypeScript will not find a language server. Nothing here imports
the API; `@effect/language-service` is loaded by the editor's own TypeScript,
not this one.
- Linting is type-aware (`oxlint --type-aware`, backed by `oxlint-tsgolint`),
which is what enforces the "No Floating Promises" and "No any" rules above —
so `.oxlintrc.json` needs the tsconfigs to typecheck before it can say
anything, and a `tsconfig-error` in its output is a config problem rather than
a lint one. `typescript/unbound-method` is off for test files only:
`expect(client.close).toHaveBeenCalled()` reads a method without calling it,
which is what vitest asks for. It stays on for source, where it found a real
one.
- `.oxfmtrc.json` holds the formatter's `ignorePatterns`, and
`src/format-ignore.test.ts` fails when they disagree with
`doctor.config.json`'s `ignore.files` — that guard exists because a
whole-repository format once reshaped the 252 KB generated bundle in `design/`
by 917 lines.
- Native packages (`pg`, `@libsql`) are externalized in `vite.main.config.ts`
- API base URL in frontend: `http://127.0.0.1:7847` (the server binds loopback
only)
Expand Down Expand Up @@ -396,11 +416,18 @@ app with `yarn start`, then drive the renderer with
[agent-browser](https://github.com/vercel-labs/agent-browser):

```bash
tail -f /dev/null | yarn start &
agent-browser --cdp 9222 snapshot -i
agent-browser --cdp 9222 click @e1
agent-browser --cdp 9222 screenshot
```

`electron-forge start` exits a few seconds after launching when stdin is not a
TTY, which takes the Vite dev server with it and leaves an orphaned Electron
still answering CDP with a blank window — hence the `tail -f /dev/null` holding
stdin open. The tell is the CDP target's title: `localhost:5173` rather than
`Squeal`.

Use `--cdp 9222` (not `--auto-connect`) so it attaches to the Electron window
rather than the user's regular Chrome. Refs invalidate after navigation or DOM
changes — re-snapshot before re-interacting.
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ free of main-process imports. See `CLAUDE.md` for the architecture in detail.
- `yarn typecheck` - Type-check both projects (backend and renderer)
- `yarn test` - Run the test suite once
- `yarn test:watch` - Run the test suite in watch mode
- `yarn lint` - Run ESLint
- `yarn format` - Format code with Prettier
- `yarn lint` - Run oxlint
- `yarn format` - Format code with oxfmt
- `yarn format:check` - Check formatting without writing (what CI runs)
- `yarn seed` - Seed the sample databases
- `yarn package` - Package the app
Expand Down
16 changes: 7 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
"description": "Ergonomic SQL Client for Humans",
"main": ".vite/build/main.js",
"scripts": {
"format": "prettier --write .",
"format:check": "prettier --check .",
"format": "oxfmt",
"format:check": "oxfmt --check",
"generate-icons": "bun run scripts/generate-icons.ts",
"lint": "eslint --ext .ts,.tsx .",
"lint": "oxlint --type-aware --deny-warnings",
"make": "electron-forge make",
"make:mac": "tsx scripts/fetch-macos-bindings.ts && electron-forge make --arch=universal",
"package": "electron-forge package",
Expand Down Expand Up @@ -110,20 +110,18 @@
"@types/react": "^19.2.6",
"@types/react-dom": "^19.2.3",
"@types/uuid": "^11.0.0",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"@vitejs/plugin-react": "^5.1.1",
"electron": "39.2.3",
"eslint": "^8.57.1",
"eslint-plugin-import": "^2.32.0",
"jsdom": "^27.2.0",
"oxfmt": "^0.68.0",
"oxlint": "^1.83.0",
"oxlint-tsgolint": "^7.0.2002",
"png-to-ico": "^3.0.1",
"postcss": "^8.5.6",
"prettier": "3.7.1",
"sharp": "^0.34.5",
"tailwindcss": "^4.1.17",
"tsx": "^4.20.6",
"typescript": "^5.7.0",
"typescript": "^7.0.2",
"vite": "^5.4.21",
"vitest": "^4.1.9"
},
Expand Down
6 changes: 5 additions & 1 deletion scripts/generate-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,8 @@ async function main() {
console.log('Done!')
}

main()
main().catch((error: unknown) => {
console.error(error)

process.exit(1)
})
6 changes: 5 additions & 1 deletion scripts/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,8 @@ async function seed() {
}
}

seed()
seed().catch((error: unknown) => {
console.error(error)

process.exit(1)
})
1 change: 0 additions & 1 deletion src/app/components/DatabaseExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { ReactElement, useCallback } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { toast } from 'sonner'

import { useCollections } from '../collections-context'
import { useConfirm } from './ConfirmDialogProvider'
import {
useDatabases,
Expand Down
6 changes: 3 additions & 3 deletions src/app/components/TitleBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import { cn } from '@/app/lib/utils'
const isMac = navigator.platform.toLowerCase().includes('mac')

const handleClose = () => {
window.electron.windowClose()
void window.electron.windowClose()
}

const handleMaximize = () => {
window.electron.windowMaximize()
void window.electron.windowMaximize()
}

const handleMinimize = () => {
window.electron.windowMinimize()
void window.electron.windowMinimize()
}

export function TitleBar(): ReactElement {
Expand Down
6 changes: 5 additions & 1 deletion src/app/components/query-result-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ export function formatCellValue(value: unknown): string {
return JSON.stringify(value)
}

return String(value)
// Every branch above has ruled out the shapes `String` renders as
// `[object Object]`, which the cast is what says.
return String(
value as string | number | bigint | boolean | symbol | undefined
)
}

export function escapeCsvField(value: string): string {
Expand Down
2 changes: 1 addition & 1 deletion src/app/hooks/use-list-reorder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ describe('useListReorder', () => {
Object.entries(result.current.dndContextProps)
.filter(([name]) => name.startsWith('onDrag'))
.map(([name, value]) => [name, typeof value])
.sort()
.sort(([first], [second]) => first.localeCompare(second))
).toEqual([
['onDragCancel', 'function'],
['onDragEnd', 'function'],
Expand Down
6 changes: 3 additions & 3 deletions src/app/tracing/exporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ describe('flushSpans', () => {

await exporter.flushSpans()

expect(send).toHaveBeenCalledTimes(2)
expect((send.mock.calls[0]?.[0] as unknown[]).length).toEqual(100)
expect((send.mock.calls[1]?.[0] as unknown[]).length).toEqual(50)
expect(
send.mock.calls.map((call) => (call[0] as unknown[]).length)
).toEqual([100, 50])
})

it('keeps spans buffered when the send fails', async () => {
Expand Down
1 change: 0 additions & 1 deletion src/database/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { pathToFileURL } from 'url'

function getUserDataPath(): string {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { app } = require('electron')

return app.isPackaged ? app.getPath('userData') : process.cwd()
Expand Down
5 changes: 3 additions & 2 deletions src/databases/sqlite-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ export class SqliteAdapter implements DatabaseAdapter {

try {
const result = await client.execute('select sqlite_version() as version')
const rawVersion = String(result.rows[0]?.version ?? '')
const version = result.rows[0]?.version
const rawVersion = typeof version === 'string' ? version : ''

return requireServerVersion(
formatSqliteServerVersion(rawVersion),
Expand Down Expand Up @@ -172,7 +173,7 @@ export class SqliteAdapter implements DatabaseAdapter {

return result.rows.map((row) => ({
columnName: row.from as string,
constraintName: `fk_${tableName}_${row.id}`,
constraintName: `fk_${tableName}_${row.id as number}`,
referencedColumnName: row.to as string,
referencedTableName: row.table as string,
referencedTableSchema: 'main'
Expand Down
Loading
Loading