Compare commits

...

1 Commits

Author SHA1 Message Date
ethernet
1c34399603 change(tooling): typecheck in CI, update ts to 6
fix(ui-tui): fix ts 6 real type errors

change(tooling): use new node everywhere
2026-06-04 20:44:45 -04:00
25 changed files with 197 additions and 88 deletions

View File

@@ -44,9 +44,9 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20
node-version: 22
cache: npm
cache-dependency-path: website/package-lock.json
cache-dependency-path: package-lock.json
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:

View File

@@ -3,8 +3,8 @@ name: Docs Site Checks
on:
pull_request:
paths:
- 'website/**'
- '.github/workflows/docs-site-checks.yml'
- "website/**"
- ".github/workflows/docs-site-checks.yml"
workflow_dispatch:
permissions:
@@ -14,21 +14,21 @@ jobs:
docs-site-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20
node-version: 22
cache: npm
cache-dependency-path: website/package-lock.json
cache-dependency-path: package-lock.json
- name: Install website dependencies
run: npm ci
working-directory: website
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.11'
python-version: "3.11"
- name: Install ascii-guard
run: python -m pip install ascii-guard==2.3.0 pyyaml==6.0.3

25
.github/workflows/typecheck.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
# .github/workflows/typecheck.yml
name: Typecheck
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
typecheck:
runs-on: ubuntu-latest
strategy:
matrix:
package:
[ui-tui, web, apps/bootstrap-installer, apps/desktop, apps/shared]
fail-fast: false # report all failures, not just the first one
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@0355742c943ddb13ca8a6b700f824231caa91e75
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run --prefix ${{ matrix.package }} typecheck

View File

@@ -264,7 +264,7 @@ npm install # first time
npm run dev # watch mode (rebuilds hermes-ink + tsx --watch)
npm start # production
npm run build # full build (hermes-ink + tsc)
npm run type-check # typecheck only (tsc --noEmit)
npm run typecheck # typecheck only (tsc --noEmit)
npm run lint # eslint
npm run fmt # prettier
npm test # vitest

View File

@@ -11,7 +11,8 @@
"tauri": "tauri",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build",
"tauri:build:debug": "tauri build --debug"
"tauri:build:debug": "tauri build --debug",
"typecheck": "tsc -p . --noEmit"
},
"dependencies": {
"@nous-research/ui": "0.16.0",
@@ -40,7 +41,7 @@
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.2.0",
"typescript": "~5.9.3",
"typescript": "^6.0.3",
"vite": "^7.3.1"
}
}

View File

@@ -16,9 +16,8 @@
"noUnusedParameters": true,
"esModuleInterop": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
"@/*": ["./src/*"]
}
},
"include": ["src"],

View File

@@ -102,7 +102,7 @@ Run before opening a PR (lint may surface pre-existing warnings but must exit cl
```bash
npm run fix
npm run type-check
npm run typecheck
npm run lint
npm run test:desktop:all
```

View File

@@ -36,7 +36,7 @@
"test:desktop:existing": "node scripts/test-desktop.mjs existing",
"test:desktop:fresh": "node scripts/test-desktop.mjs fresh",
"test:desktop:platforms": "node --test electron/bootstrap-platform.test.cjs electron/hardening.test.cjs electron/backend-probes.test.cjs electron/bootstrap-runner.test.cjs electron/connection-config.test.cjs",
"type-check": "tsc -b",
"typecheck": "tsc -p . --noEmit",
"lint": "eslint src/ electron/",
"lint:fix": "eslint src/ electron/ --fix",
"fmt": "prettier --write 'src/**/*.{ts,tsx}' 'electron/**/*.{js,cjs}' 'vite.config.ts'",
@@ -103,7 +103,7 @@
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.3.2",
"@types/hast": "^3.0.4",
"@types/node": "^24.12.2",
"@types/node": "^24.12.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.59.1",

View File

@@ -1035,8 +1035,16 @@ export function ChatBar({
}, [activeQueueSessionKey, editingQueuedPrompt, queueEdit]) // eslint-disable-line react-hooks/exhaustive-deps
const submitDraft = () => {
const trimmedDraft = draft.trim()
if (queueEdit) {
exitQueuedEdit('save')
} else if (trimmedDraft && SLASH_COMMAND_RE.test(trimmedDraft) && !attachments.length) {
// Slash commands are dispatched immediately (via onSubmit →
// executeSlashCommand), never queued — even when busy.
triggerHaptic('submit')
clearDraft()
void onSubmit(trimmedDraft)
} else if (busy) {
// Slash commands should execute immediately even while the agent is
// busy — they're client-side operations (/yolo, /skin, /new, /help,
@@ -1064,7 +1072,7 @@ export function ChatBar({
}
} else if (!hasComposerPayload && queuedPrompts.length > 0) {
void drainNextQueued()
} else if (draft.trim() || attachments.length > 0) {
} else if (trimmedDraft || attachments.length > 0) {
const submitted = draft
triggerHaptic('submit')
clearDraft()

View File

@@ -75,3 +75,58 @@ describe('blobDedupeKey', () => {
expect(blobDedupeKey(file)).toBe('file:a.png:0:image/png:42')
})
})
describe('detectTrigger', () => {
it('detects slash commands at the start of the input', () => {
const result = detectTrigger('/steer')
expect(result).toEqual({ kind: '/', query: 'steer', tokenLength: 6 })
})
it('detects partial slash commands at the start', () => {
const result = detectTrigger('/st')
expect(result).toEqual({ kind: '/', query: 'st', tokenLength: 3 })
})
it('detects bare slash at the start', () => {
const result = detectTrigger('/')
expect(result).toEqual({ kind: '/', query: '', tokenLength: 1 })
})
it('does NOT trigger slash autocomplete mid-message', () => {
const result = detectTrigger('hello /steer')
expect(result).toBeNull()
})
it('does NOT trigger slash autocomplete after a space mid-message', () => {
const result = detectTrigger('some text /new')
expect(result).toBeNull()
})
it('detects @ mentions at the start of the input', () => {
const result = detectTrigger('@user')
expect(result).toEqual({ kind: '@', query: 'user', tokenLength: 5 })
})
it('detects @ mentions mid-sentence after whitespace', () => {
const result = detectTrigger('hello @user')
expect(result).toEqual({ kind: '@', query: 'user', tokenLength: 5 })
})
it('detects @ mentions at the start of a new line', () => {
const result = detectTrigger('hello\n@user')
expect(result).toEqual({ kind: '@', query: 'user', tokenLength: 5 })
})
it('returns null for plain text without triggers', () => {
expect(detectTrigger('hello world')).toBeNull()
})
it('returns null for empty input', () => {
expect(detectTrigger('')).toBeNull()
})
it('returns null when / is embedded in a word mid-message', () => {
expect(detectTrigger('use path/to/file')).toBeNull()
})
})

View File

@@ -6,7 +6,10 @@ export interface TriggerState {
tokenLength: number
}
const TRIGGER_RE = /(?:^|[\s])([@/])([^\s@/]*)$/
// Slash commands only trigger autocomplete at the start of the input;
// @ mentions can appear mid-sentence after whitespace.
const SLASH_TRIGGER_RE = /^\/([^\s@/]*)$/
const AT_TRIGGER_RE = /(?:^|[\s])@([^\s@/]*)$/
/** Stable key for paste dedupe — `items` and `files` often mirror the same image as different objects. */
export function blobDedupeKey(blob: Blob): string {
@@ -97,11 +100,17 @@ export function textBeforeCaret(editor: HTMLDivElement): string | null {
}
export function detectTrigger(textBefore: string): TriggerState | null {
const match = TRIGGER_RE.exec(textBefore)
const slashMatch = SLASH_TRIGGER_RE.exec(textBefore)
if (!match) {
return null
if (slashMatch) {
return { kind: '/', query: slashMatch[1], tokenLength: 1 + slashMatch[1].length }
}
return { kind: match[1] as '@' | '/', query: match[2], tokenLength: 1 + match[2].length }
const atMatch = AT_TRIGGER_RE.exec(textBefore)
if (atMatch) {
return { kind: '@', query: atMatch[1], tokenLength: 1 + atMatch[1].length }
}
return null
}

View File

@@ -8,7 +8,7 @@
},
"types": "./src/index.ts",
"scripts": {
"type-check": "tsc -p tsconfig.json --noEmit"
"typecheck": "tsc -p . --noEmit"
},
"devDependencies": {
"typescript": "^6.0.3"

View File

@@ -21,7 +21,7 @@ let
# Single npm deps fetch from the workspace root lockfile.
# All workspace packages share this derivation.
npmDepsHash = "sha256-T9UtpXgBCl/GywDZyrvG4a69RkV8oD6p1UOT7GPgAS0=";
npmDepsHash = "sha256-FGd6EWIG2jBy6BvR/VhGBv2d/OGIqQ9GAHjWc4n8Q1w=";
npmDeps = pkgs.fetchNpmDeps {
inherit src;
@@ -53,7 +53,7 @@ in
{
folder, # repo-relative folder with package.json, e.g. "ui-tui"
attr, # flake package attr, e.g. "tui"
pname, # e.g. "hermes-tui"
...
}:
let
# No sourceRoot — the workspace root (with the single package-lock.json)

80
package-lock.json generated
View File

@@ -53,10 +53,24 @@
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.2.0",
"typescript": "~5.9.3",
"typescript": "^6.0.3",
"vite": "^7.3.1"
}
},
"apps/bootstrap-installer/node_modules/typescript": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"apps/desktop": {
"name": "hermes",
"version": "0.15.1",
@@ -119,7 +133,7 @@
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.3.2",
"@types/hast": "^3.0.4",
"@types/node": "^24.12.2",
"@types/node": "^24.12.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.59.1",
@@ -11506,25 +11520,6 @@
"@electron/windows-sign": "^1.1.2"
}
},
"node_modules/electron-winstaller/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/electron-winstaller/node_modules/fs-extra": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
@@ -11552,14 +11547,6 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/electron-winstaller/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/electron-winstaller/node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
@@ -20302,6 +20289,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -21459,7 +21447,7 @@
},
"devDependencies": {
"@eslint/js": "^9",
"@types/node": "^25.5.0",
"@types/node": "^24.12.0",
"@types/react": "^19.2.14",
"@typescript-eslint/eslint-plugin": "^8",
"@typescript-eslint/parser": "^8",
@@ -21473,7 +21461,7 @@
"globals": "^16",
"prettier": "^3",
"tsx": "^4.19.0",
"typescript": "^5.7.0",
"typescript": "^6.0.3",
"vitest": "^4.1.3"
}
},
@@ -22052,6 +22040,20 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"ui-tui/node_modules/typescript": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"ui-tui/node_modules/undici-types": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
@@ -22189,7 +22191,7 @@
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"three": "^0.180.0",
"typescript": "~5.9.3",
"typescript": "^6.0.3",
"typescript-eslint": "^8.56.1",
"vite": "^7.3.1"
}
@@ -22250,6 +22252,20 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"web/node_modules/typescript": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}

View File

@@ -7,7 +7,7 @@
"dev": "npm run build --prefix packages/hermes-ink && tsx --watch src/entry.tsx",
"start": "tsx src/entry.tsx",
"build": "node scripts/build.mjs",
"type-check": "tsc --noEmit -p tsconfig.json",
"typecheck": "tsc --noEmit -p tsconfig.json",
"lint": "eslint src/ packages/",
"lint:fix": "eslint src/ packages/ --fix",
"fmt": "prettier --write 'src/**/*.{ts,tsx}' 'packages/**/*.{ts,tsx}'",
@@ -26,7 +26,7 @@
},
"devDependencies": {
"@eslint/js": "^9",
"@types/node": "^25.5.0",
"@types/node": "^24.12.0",
"@types/react": "^19.2.14",
"@typescript-eslint/eslint-plugin": "^8",
"@typescript-eslint/parser": "^8",
@@ -40,7 +40,7 @@
"globals": "^16",
"prettier": "^3",
"tsx": "^4.19.0",
"typescript": "^5.7.0",
"typescript": "^6.0.3",
"vitest": "^4.1.3"
}
}

View File

@@ -1,4 +1,4 @@
import { spawn } from 'child_process'
import { spawn, SpawnOptionsWithStdioTuple } from 'child_process'
type ExecFileOptions = {
input?: string
timeout?: number
@@ -28,18 +28,18 @@ export function execFileNoThrow(
error?: string
}> {
return new Promise(resolve => {
// When resolveOnExit is true, ignore stdout/stderr so the daemon
// doesn't inherit those pipe FDs — prevents handle leaks that can
// keep the parent process alive. No output data is collected in
// this mode; both stdout and stderr will be empty strings.
const stdioConfig = options.resolveOnExit
? ['pipe', 'ignore', 'ignore'] as const
: 'pipe' as const
const child = spawn(file, args, {
cwd: options.useCwd ? process.cwd() : undefined,
env: options.env,
stdio: stdioConfig
// When resolveOnExit is true, ignore stdout/stderr so the daemon
// doesn't inherit those pipe FDs — prevents handle leaks that can
// keep the parent process alive. No output data is collected in
// this mode; both stdout and stderr will be empty strings.
stdio: options.resolveOnExit
? ['pipe', 'ignore', 'ignore']
: 'pipe'
})
let stdout = ''

View File

@@ -84,7 +84,7 @@ const asWireText = (raw: unknown): string | null => {
}
if (ArrayBuffer.isView(raw)) {
return _wireDecoder.decode(raw)
return _wireDecoder.decode(raw.buffer)
}
return null

View File

@@ -53,7 +53,7 @@ export async function readOsc52Clipboard(querier: null | OscQuerier, timeoutMs =
return null
}
const timeout = new Promise<undefined>(resolve => setTimeout(resolve, timeoutMs))
const timeout = new Promise<void>(resolve => setTimeout(resolve, timeoutMs))
const query = querier.send<OscResponse>({
request: buildOsc52ClipboardQuery(),

View File

@@ -1,7 +1,6 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@hermes/ink": ["src/types/hermes-ink.d.ts"]
}

View File

@@ -1,6 +1,7 @@
{
"compilerOptions": {
"target": "ES2022",
"target": "ES2023",
"lib": ["ES2023"],
"module": "nodenext",
"moduleResolution": "nodenext",
"jsx": "react-jsx",

View File

@@ -7,7 +7,8 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
"preview": "vite preview",
"typecheck": "tsc -p . --noEmit"
},
"dependencies": {
"@nous-research/ui": "0.18.2",
@@ -35,7 +36,7 @@
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@types/node": "^24.12.0",
"@types/node": "^24.12.0",
"@types/qrcode": "^1.5.6",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
@@ -45,7 +46,7 @@
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"three": "^0.180.0",
"typescript": "~5.9.3",
"typescript": "^6.0.3",
"typescript-eslint": "^8.56.1",
"vite": "^7.3.1"
}

View File

@@ -17,7 +17,6 @@
"jsx": "react-jsx",
/* Path aliases */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},

View File

@@ -22,7 +22,7 @@
"@docusaurus/module-type-aliases": "3.9.2",
"@docusaurus/tsconfig": "3.9.2",
"@docusaurus/types": "3.9.2",
"typescript": "~5.6.2"
"typescript": "^6.0.3"
},
"engines": {
"node": ">=20.0"
@@ -18926,9 +18926,9 @@
}
},
"node_modules/typescript": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {

View File

@@ -14,7 +14,6 @@
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc",
"lint:diagrams": "ascii-guard lint --exclude-code-blocks docs"
},
"dependencies": {
@@ -32,7 +31,7 @@
"@docusaurus/module-type-aliases": "3.9.2",
"@docusaurus/tsconfig": "3.9.2",
"@docusaurus/types": "3.9.2",
"typescript": "~5.6.2"
"typescript": "^6.0.3"
},
"overrides": {
"serialize-javascript": "^7.0.5",

View File

@@ -1,8 +1,5 @@
{
// This file is not used in compilation. It is here just for a nice editor experience.
"extends": "@docusaurus/tsconfig",
"compilerOptions": {
"baseUrl": "."
},
"exclude": [".docusaurus", "build"]
}