From 6881151cc19d5926cd4d0485d1a2f2012cee8821 Mon Sep 17 00:00:00 2001 From: Joydip Roy Date: Sat, 21 Mar 2026 13:01:30 +0530 Subject: [PATCH 1/6] feat(theme): add validation and fallback handling for theme loading - Validate light/dark variants and seed structure in validateTheme - Fall back to light mode if requested mode is missing in getTheme - Refactor loadTheme to support custom theme objects and allowlist - Update dependencies and postinstall script --- README.md | 47 +++++++++++++++-- bun.lock | 80 ++++++++++++++--------------- package.json | 7 +-- src/container.ts | 7 +-- src/theme.ts | 128 ++++++++++++++++++++++++++++++++++++++++------- 5 files changed, 197 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index e23ab8a..45e1124 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,52 @@ # try-opentui -To install dependencies: +## Project Overview + +This is a Bun-based TypeScript TUI (Terminal User Interface) application using `@opentui/core` for rendering. + +## Development & CLI Commands Guide + +### Installation & Running + +```bash +bun install # Install dependencies +bun dev # Run in development mode with file watching +bun start # Run the application +``` + +### Linting & Formatting ```bash -bun install +bun lint # Run oxlint to check code quality +bun lint:fix # Run oxlint with auto-fix +bun format # Run oxfmt to format code ``` -To run: +### Type Checking & Pre-commit ```bash -bun run index.ts +bun typecheck # Run TypeScript type checking (tsgo) +bun run actions:up # Run actions-up (updates?) ``` -This project was created using `bun init` in bun v1.3.9. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. +**Note:** There is no test framework configured in this project yet. + +--- + +## File Organization + +```bash +├── main.ts # Entry point +├── src/ +│ ├── config.ts # Configuration management +│ ├── container.ts # Main UI container component +│ ├── renderer.ts # Renderer lifecycle management +│ ├── theme.ts # Theme loading and management +│ ├── utils.ts # Utility functions +│ └── figures.ts # Figure/shape utilities +├── docs/ # Documentation +├── themes/ # Theme JSON files +├── .oxlintrc.json # Linting rules +├── .oxfmtrc.json # Formatting rules +└── tsconfig.json # TypeScript configuration +``` diff --git a/bun.lock b/bun.lock index 76caae7..bfa6d6c 100644 --- a/bun.lock +++ b/bun.lock @@ -5,14 +5,14 @@ "": { "name": "try-opentui", "dependencies": { - "@opentui/core": "^0.1.81", + "@opentui/core": "^0.1.90", }, "devDependencies": { "@types/bun": "latest", - "@typescript/native-preview": "^7.0.0-dev.20260223.1", + "@typescript/native-preview": "^7.0.0-dev.20260320.1", "actions-up": "^1.12.0", "oxfmt": "^0.35.0", - "oxlint": "^1.50.0", + "oxlint": "^1.56.0", "simple-git-hooks": "^2.13.1", }, "peerDependencies": { @@ -79,19 +79,19 @@ "@jimp/utils": ["@jimp/utils@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "tinycolor2": "^1.6.0" } }, "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA=="], - "@opentui/core": ["@opentui/core@0.1.83", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.83", "@opentui/core-darwin-x64": "0.1.83", "@opentui/core-linux-arm64": "0.1.83", "@opentui/core-linux-x64": "0.1.83", "@opentui/core-win32-arm64": "0.1.83", "@opentui/core-win32-x64": "0.1.83", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-8r0YoYe1gMXGg6h2M7xadHTI/DD+7PZccK+SgMbpFlLNiH2U/meiGCuDkjxl6N0uyiFHezEFJiCfXAM7u0Z45g=="], + "@opentui/core": ["@opentui/core@0.1.90", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.90", "@opentui/core-darwin-x64": "0.1.90", "@opentui/core-linux-arm64": "0.1.90", "@opentui/core-linux-x64": "0.1.90", "@opentui/core-win32-arm64": "0.1.90", "@opentui/core-win32-x64": "0.1.90", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-Os2dviqWVETU3kaK36lbSvdcI93GAWhw0xb9ng/d0DWYuM9scRmAhLHiOayp61saWv/BR8OJXeuQYHvrp5rd6A=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.83", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wU3/5pzXQajuxYfsZOA+3/Mywocmu4S1PPEqW9l3PVm9/MGaCWsPq4p9xUuNV4ukOPQhs42TyIHpYpdRleA1sA=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.90", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XFrm2zCg1SlHPQ5A2HX/I4dCrmTjYaCJIIpo3QuPIvZBGH3aBMdWDJh2tXw7AB5Mmh8X1K4hDkP5nlK9x0Ewow=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.83", "", { "os": "darwin", "cpu": "x64" }, "sha512-xyxqRKxMFCJDJ4sANajhDaznACXPoRBreAZsDAAb/cLirqKdJzQ7gVmInwOwAURTWp/lDe/ryC7FLD9e3V/C9Q=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.90", "", { "os": "darwin", "cpu": "x64" }, "sha512-vbDpUsnlZ+0CeVKyBBXE+l2+X1XoVncMxMOhXTiMtud2/Cwu+Vfs/g3LC/6Zv08yaytA+9g7Z8sdf0QCqFyQ4w=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.83", "", { "os": "linux", "cpu": "arm64" }, "sha512-9KdFjks7TYGgyGHpuKO1hBiAKEGVNkd686L8GyoVJFHfIKdTIqfnaOUhVTwTiH0WcBT6A7VP/HjAaJqEZKyd6w=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.90", "", { "os": "linux", "cpu": "arm64" }, "sha512-OTbvBTP5mVQ4uwKyuz6b59ElG+D0i1Ln+q6cVhNkLgeRLySIn1uXEzUFQGlnVgb8lFDANsn3yQmdv+R+Cpw0og=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.83", "", { "os": "linux", "cpu": "x64" }, "sha512-rLAGX/fAnpTPylNaMCY1HySe8xFJdXyJXPmDdfsT5VuilIn10YJ+OncrJrq+vYk52avU+AJfnPly4M4lsWGRpw=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.90", "", { "os": "linux", "cpu": "x64" }, "sha512-2PJi/LLlO7tGk9Ful/n+6iBdg1RFrA9ibU7wVneE6Z1P0LCYeu7bpwMzea1TXL0eAQWPHsjTs9aPlqPxln0EJw=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.83", "", { "os": "win32", "cpu": "arm64" }, "sha512-m0r3PNkogwzQS0GhL2HvyCbcRgcmEz2NjDRS1GpPGeEKQKfLHBpfwG8GQ+wtrnm7C1LSlcN+1rPXpYgq2FNvJA=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.90", "", { "os": "win32", "cpu": "arm64" }, "sha512-+sTRaOb7gCMZ6iLuuG4y9kzyweJzBDcIJN0Xh49ikFWTwVECDXEVtXahNGlw57avm2yYUoNzmpBjK/LV7zBj9A=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.83", "", { "os": "win32", "cpu": "x64" }, "sha512-d4ltQln60iyy+DS0WRKzUqZ4QCVzO61+jaEWJPfsiNMBstjxlkmqXuV8/gLCHMPKCpy7RDtGiWGDOpGSLTsZuA=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.90", "", { "os": "win32", "cpu": "x64" }, "sha512-aVFyErckWp4oW9NJ/ZDKBUAlTlfVUiRXGP63JXFOoeqI7EYaM8uBt6rgZAJuUdFWCN2Q66WRS8Y2mk+0BJwVBg=="], "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.35.0", "", { "os": "android", "cpu": "arm" }, "sha512-BaRKlM3DyG81y/xWTsE6gZiv89F/3pHe2BqX2H4JbiB8HNVlWWtplzgATAE5IDSdwChdeuWLDTQzJ92Lglw3ZA=="], @@ -131,65 +131,65 @@ "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.35.0", "", { "os": "win32", "cpu": "x64" }, "sha512-WCDJjlS95NboR0ugI2BEwzt1tYvRDorDRM9Lvctls1SLyKYuNRCyrPwp1urUPFBnwgBNn9p2/gnmo7gFMySRoQ=="], - "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.50.0", "", { "os": "android", "cpu": "arm" }, "sha512-G7MRGk/6NCe+L8ntonRdZP7IkBfEpiZ/he3buLK6JkLgMHgJShXZ+BeOwADmspXez7U7F7L1Anf4xLSkLHiGTg=="], + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-IyfYPthZyiSKwAv/dLjeO18SaK8MxLI9Yss2JrRDyweQAkuL3LhEy7pwIwI7uA3KQc1Vdn20kdmj3q0oUIQL6A=="], - "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.50.0", "", { "os": "android", "cpu": "arm64" }, "sha512-GeSuMoJWCVpovJi/e3xDSNgjeR8WEZ6MCXL6EtPiCIM2NTzv7LbflARINTXTJy2oFBYyvdf/l2PwHzYo6EdXvg=="], + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.56.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Ga5zYrzH6vc/VFxhn6MmyUnYEfy9vRpwTIks99mY3j6Nz30yYpIkWryI0QKPCgvGUtDSXVLEaMum5nA+WrNOSg=="], - "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.50.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-w3SY5YtxGnxCHPJ8Twl3KmS9oja1gERYk3AMoZ7Hv8P43ZtB6HVfs02TxvarxfL214Tm3uzvc2vn+DhtUNeKnw=="], + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.56.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ogmbdJysnw/D4bDcpf1sPLpFThZ48lYp4aKYm10Z/6Nh1SON6NtnNhTNOlhEY296tDFItsZUz+2tgcSYqh8Eyw=="], - "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.50.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-hNfogDqy7tvmllXKBSlHo6k5x7dhTUVOHbMSE15CCAcXzmqf5883aPvBYPOq9AE7DpDUQUZ1kVE22YbiGW+tuw=="], + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.56.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-x8QE1h+RAtQ2g+3KPsP6Fk/tdz6zJQUv5c7fTrJxXV3GHOo+Ry5p/PsogU4U+iUZg0rj6hS+E4xi+mnwwlDCWQ=="], - "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.50.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ykZevOWEyu0nsxolA911ucxpEv0ahw8jfEeGWOwwb/VPoE4xoexuTOAiPNlWZNJqANlJl7yp8OyzCtXTUAxotw=="], + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.56.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6G+WMZvwJpMvY7my+/SHEjb7BTk/PFbePqLpmVmUJRIsJMy/UlyYqjpuh0RCgYYkPLcnXm1rUM04kbTk8yS1Yg=="], - "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-hif3iDk7vo5GGJ4OLCCZAf2vjnU9FztGw4L0MbQL0M2iY9LKFtDMMiQAHmkF0PQGQMVbTYtPdXCLKVgdkiqWXQ=="], + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-YYHBsk/sl7fYwQOok+6W5lBPeUEvisznV/HZD2IfZmF3Bns6cPC3Z0vCtSEOaAWTjYWN3jVsdu55jMxKlsdlhg=="], - "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.50.0", "", { "os": "linux", "cpu": "arm" }, "sha512-dVp9iSssiGAnTNey2Ruf6xUaQhdnvcFOJyRWd/mu5o2jVbFK15E5fbWGeFRfmuobu5QXuROtFga44+7DOS3PLg=="], + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+AZK8rOUr78y8WT6XkDb04IbMRqauNV+vgT6f8ZLOH8wnpQ9i7Nol0XLxAu+Cq7Sb+J9wC0j6Km5hG8rj47/yQ=="], - "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1cT7yz2HA910CKA9NkH1ZJo50vTtmND2fkoW1oyiSb0j6WvNtJ0Wx2zoySfXWc/c+7HFoqRK5AbEoL41LOn9oA=="], + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-urse2SnugwJRojUkGSSeH2LPMaje5Q50yQtvtL9HFckiyeqXzoFwOAZqD5TR29R2lq7UHidfFDM9EGcchcbb8A=="], - "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.50.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-++B3k/HEPFVlj89cOz8kWfQccMZB/aWL9AhsW7jPIkG++63Mpwb2cE9XOEsd0PATbIan78k2Gky+09uWM1d/gQ=="], + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-rkTZkBfJ4TYLjansjSzL6mgZOdN5IvUnSq3oNJSLwBcNvy3dlgQtpHPrRxrCEbbcp7oQ6If0tkNaqfOsphYZ9g=="], - "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.50.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Z9b/KpFMkx66w3gVBqjIC1AJBTZAGoI9+U+K5L4QM0CB/G0JSNC1es9b3Y0Vcrlvtdn8A+IQTkYjd/Q0uCSaZw=="], + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-uqL1kMH3u69/e1CH2EJhP3CP28jw2ExLsku4o8RVAZ7fySo9zOyI2fy9pVlTAp4voBLVgzndXi3SgtdyCTa2aA=="], - "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-jvmuIw8wRSohsQlFNIST5uUwkEtEJmOQYr33bf/K2FrFPXHhM4KqGekI3ShYJemFS/gARVacQFgBzzJKCAyJjg=="], + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-j0CcMBOgV6KsRaBdsebIeiy7hCjEvq2KdEsiULf2LZqAq0v1M1lWjelhCV57LxsqaIGChXFuFJ0RiFrSRHPhSg=="], - "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.50.0", "", { "os": "linux", "cpu": "none" }, "sha512-x+UrN47oYNh90nmAAyql8eQaaRpHbDPu5guasDg10+OpszUQ3/1+1J6zFMmV4xfIEgTcUXG/oI5fxJhF4eWCNA=="], + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-7VDOiL8cDG3DQ/CY3yKjbV1c4YPvc4vH8qW09Vv+5ukq3l/Kcyr6XGCd5NvxUmxqDb2vjMpM+eW/4JrEEsUetA=="], - "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.50.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-i/JLi2ljLUIVfekMj4ISmdt+Hn11wzYUdRRrkVUYsCWw7zAy5xV7X9iA+KMyM156LTFympa7s3oKBjuCLoTAUQ=="], + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.56.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-JGRpX0M+ikD3WpwJ7vKcHKV6Kg0dT52BW2Eu2BupXotYeqGXBrbY+QPkAyKO6MNgKozyTNaRh3r7g+VWgyAQYQ=="], - "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-/C7brhn6c6UUPccgSPCcpLQXcp+xKIW/3sji/5VZ8/OItL3tQ2U7KalHz887UxxSQeEOmd1kY6lrpuwFnmNqOA=="], + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-dNaICPvtmuxFP/VbqdofrLqdS3bM/AKJN3LMJD52si44ea7Be1cBk6NpfIahaysG9Uo+L98QKddU9CD5L8UHnQ=="], - "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.50.0", "", { "os": "linux", "cpu": "x64" }, "sha512-oDR1f+bGOYU8LfgtEW8XtotWGB63ghtcxk5Jm6IDTCk++rTA/IRMsjOid2iMd+1bW+nP9Mdsmcdc7VbPD3+iyQ=="], + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pF1vOtM+GuXmbklM1hV8WMsn6tCNPvkUzklj/Ej98JhlanbmA2RB1BILgOpwSuCTRTIYx2MXssmEyQQ90QF5aA=="], - "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.50.0", "", { "os": "none", "cpu": "arm64" }, "sha512-4CmRGPp5UpvXyu4jjP9Tey/SrXDQLRvZXm4pb4vdZBxAzbFZkCyh0KyRy4txld/kZKTJlW4TO8N1JKrNEk+mWw=="], + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.56.0", "", { "os": "none", "cpu": "arm64" }, "sha512-bp8NQ4RE6fDIFLa4bdBiOA+TAvkNkg+rslR+AvvjlLTYXLy9/uKAYLQudaQouWihLD/hgkrXIKKzXi5IXOewwg=="], - "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.50.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Fq0M6vsGcFsSfeuWAACDhd5KJrO85ckbEfe1EGuBj+KPyJz7KeWte2fSFrFGmNKNXyhEMyx4tbgxiWRujBM2KQ=="], + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.56.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-PxT4OJDfMOQBzo3OlzFb9gkoSD+n8qSBxyVq2wQSZIHFQYGEqIRTo9M0ZStvZm5fdhMqaVYpOnJvH2hUMEDk/g=="], - "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.50.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-qTdWR9KwY/vxJGhHVIZG2eBOhidOQvOwzDxnX+jhW/zIVacal1nAhR8GLkiywW8BIFDkQKXo/zOfT+/DY+ns/w=="], + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.56.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-PTRy6sIEPqy2x8PTP1baBNReN/BNEFmde0L+mYeHmjXE1Vlcc9+I5nsqENsB2yAm5wLkzPoTNCMY/7AnabT4/A=="], - "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.50.0", "", { "os": "win32", "cpu": "x64" }, "sha512-682t7npLC4G2Ca+iNlI9fhAKTcFPYYXJjwoa88H4q+u5HHHlsnL/gHULapX3iqp+A8FIJbgdylL5KMYo2LaluQ=="], + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZHa0clocjLmIDr+1LwoWtxRcoYniAvERotvwKUYKhH41NVfl0Y4LNbyQkwMZzwDvKklKGvGZ5+DAG58/Ik47tQ=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], - "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260226.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260226.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260226.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260226.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260226.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260226.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260226.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260226.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-DMxMGOB5cucSpvS/JUmKWr+gVz2J7YXZ7m5t9pjhXCcg/XuACqH5fpzea0cAoaMdT0WMxV9eOtHkK8ENLMPSig=="], + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260320.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260320.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260320.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260320.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260320.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260320.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260320.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260320.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-uLkkdYwvJQ/atPqMW9RUt1j71lcbL53e7a+G/I3AXb5QioMdWkG6nbNfKPM+06RgXwPDyDV9+bI0FT/13BEVgg=="], - "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260226.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-L2hfvBrQTjRzeUnW4QiZNaer1E9nAWNMD61fIJAfqQxlJNlfCOr7kt2tIwNamWzN56EFs+bIulGrTDx79DTFjQ=="], + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260320.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9zadqk/wJ+4bCheapoP/wZZxIGbHotKj+QDvwlCeB93LFrcuIYDt8q91xW1tzrc12YrUTgWXqJuYHTNZ9fzyHw=="], - "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260226.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-TnYwjhGlS0gNRgSN5U/JTzjdeVqxArO49T4kUfcLJTFIApVoWbnUnwl0IiF3FBL1uwW5b7BvTpqxFNBipaZnAg=="], + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260320.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-uw3sflQVaIIJhfu6xtFFogE8aC0MQ4xt9yRx2CDngp8/PQ0Qp8xp0LrUigQHbwtt7Y8AKR3YCRaX53rbovj4dg=="], - "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260226.1", "", { "os": "linux", "cpu": "arm" }, "sha512-9GDgDvh4cXPiknuEye9/UXe0i5mjC29jusPUzwgc7sPJTQaibn+t0I+lJ+GmLfAvWjKojFDbWa0+hf8FUeq6cw=="], + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260320.1", "", { "os": "linux", "cpu": "arm" }, "sha512-tCKwcnikt4uvSfHc14A6FJwEQox1yTsKcssTsAhxycH2bYuUr05AQNUdud3W41tr0MySHrcQG4CstzFxE9C86w=="], - "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260226.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-rWDc57vib1l8YAR0+Ur5BVRBJxzHpn2oG5EMFyOsQMzvjETxDHmlUm2LbR4Ma0h/jK5zuq81w39V07lK3e20fw=="], + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260320.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-fmi8apETnFd6KhLzcGFLUcPRTZO+6vHnFw0Yz08IT68aX2Vft91LnxlJuuaiKVfzE8w9fxQlU1lAZyTVWARGIg=="], - "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260226.1", "", { "os": "linux", "cpu": "x64" }, "sha512-UKMpuliKsogcB5WmysOEnfv5LTt15ABz7aRfKXqlFUxiSWqt2Hq2kGBxiLVh4rJVzKpc20850Yx+bMxzutVQ7Q=="], + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260320.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ouNWS+eUHna/mxP0tLQRzBfR5CwldCrG9mi+pTZY1pKnjvvu7b0i6OOFh9em36wIsDCjPGu6dQiv2sfBCCyRwA=="], - "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260226.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-5X5JXtUhEIiPB8ArA1mBJTvYPbj+Q3c0zh16QWXRQmIFhrnRa5EeDcyredYPtUCpr+w10SiTxWGOfkvV88mI6A=="], + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260320.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-fG32mrKJOW8U9pq3VdHNXCfxCN3T7Uh5zkf3beH3mFIRtIv65BUONwW6rh63DCtyGZTeBkF6GTJO2nIIfx3dbA=="], - "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260226.1", "", { "os": "win32", "cpu": "x64" }, "sha512-EyfJWVZgiLiU2k9VK7q4cPAZVN4w5smGwDJYiwPES1f4/aweFmengLLYK0Bsep4DVLDCitNm6TwRfWfZpKxfWw=="], + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260320.1", "", { "os": "win32", "cpu": "x64" }, "sha512-iklQlFdDWkV62GbxqVltm7y148vL6TC18BYZRzRaJjf2r+Gd2UGW3FPydNwKViTRfvWWwNx0aU8fnqTaoMBGqw=="], "@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="], @@ -213,7 +213,7 @@ "bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="], - "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], "bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="], @@ -259,7 +259,7 @@ "oxfmt": ["oxfmt@0.35.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.35.0", "@oxfmt/binding-android-arm64": "0.35.0", "@oxfmt/binding-darwin-arm64": "0.35.0", "@oxfmt/binding-darwin-x64": "0.35.0", "@oxfmt/binding-freebsd-x64": "0.35.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.35.0", "@oxfmt/binding-linux-arm-musleabihf": "0.35.0", "@oxfmt/binding-linux-arm64-gnu": "0.35.0", "@oxfmt/binding-linux-arm64-musl": "0.35.0", "@oxfmt/binding-linux-ppc64-gnu": "0.35.0", "@oxfmt/binding-linux-riscv64-gnu": "0.35.0", "@oxfmt/binding-linux-riscv64-musl": "0.35.0", "@oxfmt/binding-linux-s390x-gnu": "0.35.0", "@oxfmt/binding-linux-x64-gnu": "0.35.0", "@oxfmt/binding-linux-x64-musl": "0.35.0", "@oxfmt/binding-openharmony-arm64": "0.35.0", "@oxfmt/binding-win32-arm64-msvc": "0.35.0", "@oxfmt/binding-win32-ia32-msvc": "0.35.0", "@oxfmt/binding-win32-x64-msvc": "0.35.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-QYeXWkP+aLt7utt5SLivNIk09glWx9QE235ODjgcEZ3sd1VMaUBSpLymh6ZRCA76gD2rMP4bXanUz/fx+nLM9Q=="], - "oxlint": ["oxlint@1.50.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.50.0", "@oxlint/binding-android-arm64": "1.50.0", "@oxlint/binding-darwin-arm64": "1.50.0", "@oxlint/binding-darwin-x64": "1.50.0", "@oxlint/binding-freebsd-x64": "1.50.0", "@oxlint/binding-linux-arm-gnueabihf": "1.50.0", "@oxlint/binding-linux-arm-musleabihf": "1.50.0", "@oxlint/binding-linux-arm64-gnu": "1.50.0", "@oxlint/binding-linux-arm64-musl": "1.50.0", "@oxlint/binding-linux-ppc64-gnu": "1.50.0", "@oxlint/binding-linux-riscv64-gnu": "1.50.0", "@oxlint/binding-linux-riscv64-musl": "1.50.0", "@oxlint/binding-linux-s390x-gnu": "1.50.0", "@oxlint/binding-linux-x64-gnu": "1.50.0", "@oxlint/binding-linux-x64-musl": "1.50.0", "@oxlint/binding-openharmony-arm64": "1.50.0", "@oxlint/binding-win32-arm64-msvc": "1.50.0", "@oxlint/binding-win32-ia32-msvc": "1.50.0", "@oxlint/binding-win32-x64-msvc": "1.50.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.14.1" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-iSJ4IZEICBma8cZX7kxIIz9PzsYLF2FaLAYN6RKu7VwRVKdu7RIgpP99bTZaGl//Yao7fsaGZLSEo5xBrI5ReQ=="], + "oxlint": ["oxlint@1.56.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.56.0", "@oxlint/binding-android-arm64": "1.56.0", "@oxlint/binding-darwin-arm64": "1.56.0", "@oxlint/binding-darwin-x64": "1.56.0", "@oxlint/binding-freebsd-x64": "1.56.0", "@oxlint/binding-linux-arm-gnueabihf": "1.56.0", "@oxlint/binding-linux-arm-musleabihf": "1.56.0", "@oxlint/binding-linux-arm64-gnu": "1.56.0", "@oxlint/binding-linux-arm64-musl": "1.56.0", "@oxlint/binding-linux-ppc64-gnu": "1.56.0", "@oxlint/binding-linux-riscv64-gnu": "1.56.0", "@oxlint/binding-linux-riscv64-musl": "1.56.0", "@oxlint/binding-linux-s390x-gnu": "1.56.0", "@oxlint/binding-linux-x64-gnu": "1.56.0", "@oxlint/binding-linux-x64-musl": "1.56.0", "@oxlint/binding-openharmony-arm64": "1.56.0", "@oxlint/binding-win32-arm64-msvc": "1.56.0", "@oxlint/binding-win32-ia32-msvc": "1.56.0", "@oxlint/binding-win32-x64-msvc": "1.56.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.15.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-Q+5Mj5PVaH/R6/fhMMFzw4dT+KPB+kQW4kaL8FOIq7tfhlnEVp6+3lcWqFruuTNlUo9srZUW3qH7Id4pskeR6g=="], "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], diff --git a/package.json b/package.json index 1da40f5..d499d28 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "main": "main.ts", "module": "main.ts", "scripts": { + "postinstall": "[ -d .opencode ] && cd .opencode && bun install || true", "prepare": "simple-git-hooks", "lint": "oxlint .", "lint:fix": "oxlint --fix .", @@ -18,14 +19,14 @@ "start": "bun main.ts" }, "dependencies": { - "@opentui/core": "^0.1.83" + "@opentui/core": "^0.1.90" }, "devDependencies": { "@types/bun": "latest", - "@typescript/native-preview": "^7.0.0-dev.20260226.1", + "@typescript/native-preview": "^7.0.0-dev.20260320.1", "actions-up": "^1.12.0", "oxfmt": "^0.35.0", - "oxlint": "^1.50.0", + "oxlint": "^1.56.0", "simple-git-hooks": "^2.13.1" }, "peerDependencies": { diff --git a/src/container.ts b/src/container.ts index 8c394ef..8ca6639 100644 --- a/src/container.ts +++ b/src/container.ts @@ -47,7 +47,7 @@ export class Container implements IContainer { const queryPrefix = new BoxRenderable(this.renderer, { id: "query-prefix", backgroundColor: _theme.seeds.primary, - }) + }); const queryInput = new InputRenderable(this.renderer, { id: "query-input", @@ -60,11 +60,6 @@ export class Container implements IContainer { if (key.name === "escape") { queryInput.blur(); } - - console.log("Key pressed:", key.name); - }, - onPaste: (event) => { - console.log("Pasted:", event.text); }, }); diff --git a/src/theme.ts b/src/theme.ts index cb9e0b3..e5e8139 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -1,6 +1,39 @@ import { existsSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; +const ALLOWED_THEMES = ["default", "nord", "dracula", "nightowl"] as const; + +const DEFAULT_THEME: ColorTheme = { + name: "Default", + id: "default", + light: { + seeds: { + neutral: "#f8f8f2", + primary: "#7c6bf5", + success: "#2fbf71", + warning: "#f7a14d", + error: "#d9536f", + info: "#1d7fc5", + interactive: "#7c6bf5", + diffAdd: "#9fe3b3", + diffDelete: "#f8a1b8", + }, + }, + dark: { + seeds: { + neutral: "#1d1e28", + primary: "#bd93f9", + success: "#50fa7b", + warning: "#ffb86c", + error: "#ff5555", + info: "#8be9fd", + interactive: "#bd93f9", + diffAdd: "#2fb27d", + diffDelete: "#ff6b81", + }, + }, +}; + export type HexColor = `#${string}`; export type CssVarRef = `var(--${string})`; export type ColorValue = HexColor | CssVarRef; @@ -53,25 +86,79 @@ export class AgentTheme implements IAgentTheme { } private loadTheme(options: AgentThemeOpts): ColorTheme { - const THEME_PATH: string = resolve( - process.cwd(), - "themes", - `${options.themeName ?? "default"}.json`, - ); - - if (existsSync(THEME_PATH)) { - try { - const themeFile = readFileSync(THEME_PATH, "utf-8"); - return JSON.parse(themeFile); - } catch (error) { - console.error( - `Failed to load theme '${options.themeName}'. Falling back to default.`, - error, - ); - return JSON.parse(readFileSync(THEME_PATH, "utf-8")); + if (options.theme) { + return this.validateTheme(options.theme); + } + + const themeName = options.themeName ?? "default"; + + if (!ALLOWED_THEMES.includes(themeName as (typeof ALLOWED_THEMES)[number])) { + console.error( + `Invalid theme name '${themeName}'. Allowed themes: ${ALLOWED_THEMES.join(", ")}. Using default.`, + ); + return DEFAULT_THEME; + } + + const THEME_PATH: string = resolve(process.cwd(), "themes", `${themeName}.json`); + + if (!existsSync(THEME_PATH)) { + console.error(`Theme file not found: ${THEME_PATH}. Using default theme.`); + return DEFAULT_THEME; + } + + try { + const themeFile = readFileSync(THEME_PATH, "utf-8"); + const parsed = JSON.parse(themeFile); + return this.validateTheme(parsed); + } catch (error) { + console.error(`Failed to parse theme '${themeName}':`, error); + return DEFAULT_THEME; + } + } + + private validateTheme(theme: unknown): ColorTheme { + if ( + typeof theme === "object" && + theme !== null && + "name" in theme && + "id" in theme && + "light" in theme && + "dark" in theme + ) { + const t = theme as ColorTheme; + const validSeedKeys: (keyof ThemeSeedColors)[] = [ + "neutral", + "primary", + "success", + "warning", + "error", + "info", + "interactive", + "diffAdd", + "diffDelete", + ]; + const isValidSeeds = (seeds: unknown): seeds is ThemeSeedColors => { + if (typeof seeds !== "object" || seeds === null) return false; + return validSeedKeys.every((k) => { + const val = (seeds as Record)[k]; + return typeof val === "string" && /^#[0-9a-fA-F]{6}$/.test(val); + }); + }; + const isValidVariant = (variant: unknown): variant is ThemeVariant => { + if (typeof variant !== "object" || variant === null) return false; + const v = variant as Record; + return "seeds" in v && isValidSeeds(v["seeds"]); + }; + if (isValidVariant(t.light) && isValidVariant(t.dark)) { + return t; } + console.error( + "Theme validation failed: light/dark variants must have valid seeds. Using default.", + ); + return DEFAULT_THEME; } - return options.theme ?? JSON.parse(readFileSync(THEME_PATH, "utf-8")); + console.error("Theme validation failed: missing required fields. Using default."); + return DEFAULT_THEME; } getOptions(): AgentThemeOpts { @@ -84,7 +171,12 @@ export class AgentTheme implements IAgentTheme { */ getTheme(): ThemeVariant { const mode = this.options.mode ?? "light"; - return this.theme[mode]; + const variant = this.theme[mode]; + if (!variant) { + console.error(`Theme mode '${mode}' not found. Falling back to 'light'.`); + return this.theme["light"]; + } + return variant; } getThemeMetadata(): Pick { From 10ee40da18dea0a311d76400addeb893be42444b Mon Sep 17 00:00:00 2001 From: Joydip Roy Date: Sat, 21 Mar 2026 13:02:37 +0530 Subject: [PATCH 2/6] docs: add architecture, code style docs and opencode config - Add AGENTS.md development guide - Add ARCHITECTURE.md and CODE_STYLE.md documentation - Add opencode configuration and plugins --- .opencode/agents/docs-writer.md | 15 ++ .opencode/agents/land-pr.md | 78 ++++++ .opencode/agents/review.md | 18 ++ .opencode/agents/security-auditor.md | 17 ++ .opencode/package.json | 5 + .opencode/plugins/custom-compaction.ts | 20 ++ .opencode/skills/git-release/SKILL.md | 20 ++ .opencode/tools/project.ts | 11 + AGENTS.md | 25 ++ docs/ARCHITECTURE.md | 39 +++ docs/CODE_STYLE.md | 81 +++++++ docs/Code Patterns & Conventions.md | 22 ++ docs/Project Analysis.md | 323 +++++++++++++++++++++++++ opencode.json | 38 +++ 14 files changed, 712 insertions(+) create mode 100644 .opencode/agents/docs-writer.md create mode 100644 .opencode/agents/land-pr.md create mode 100644 .opencode/agents/review.md create mode 100644 .opencode/agents/security-auditor.md create mode 100644 .opencode/package.json create mode 100644 .opencode/plugins/custom-compaction.ts create mode 100644 .opencode/skills/git-release/SKILL.md create mode 100644 .opencode/tools/project.ts create mode 100644 AGENTS.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/CODE_STYLE.md create mode 100644 docs/Code Patterns & Conventions.md create mode 100644 docs/Project Analysis.md create mode 100644 opencode.json diff --git a/.opencode/agents/docs-writer.md b/.opencode/agents/docs-writer.md new file mode 100644 index 0000000..8598555 --- /dev/null +++ b/.opencode/agents/docs-writer.md @@ -0,0 +1,15 @@ +--- +description: Writes and maintains project documentation +mode: subagent +tools: + bash: false +--- + +You are a technical writer. Create clear, comprehensive documentation. + +Focus on: + +- Clear explanations +- Proper structure +- Code examples +- User-friendly language diff --git a/.opencode/agents/land-pr.md b/.opencode/agents/land-pr.md new file mode 100644 index 0000000..e1e10eb --- /dev/null +++ b/.opencode/agents/land-pr.md @@ -0,0 +1,78 @@ +--- +description: Land a PR (merge with proper workflow) +mode: subagent +tools: + write: false + edit: false +--- + +# Input + +- PR: $1 + - If missing: use the most recent PR mentioned in the conversation. + - If ambiguous: ask. + +Do (end-to-end) + +> Goal: PR must end in GitHub state = MERGED (never CLOSED). Prefer `gh pr merge --squash`; use `--rebase` only when preserving commit history is required. + +1. Assign PR to self: + - `gh pr edit --add-assignee @me` +2. Repo clean: `git status`. +3. Identify PR meta (author + head branch): + + ```sh + gh pr view --json number,title,author,headRefName,baseRefName,headRepository --jq '{number,title,author:.author.login,head:.headRefName,base:.baseRefName,headRepo:.headRepository.nameWithOwner}' + contrib=$(gh pr view --json author --jq .author.login) + head=$(gh pr view --json headRefName --jq .headRefName) + head_repo_url=$(gh pr view --json headRepository --jq .headRepository.url) + ``` + +4. Fast-forward base: + - `git checkout main` + - `git pull --ff-only` +5. Create temp base branch from main: + - `git checkout -b temp/landpr-` +6. Check out PR branch locally: + - `gh pr checkout ` +7. Rebase PR branch onto temp base: + - `git rebase temp/landpr-` + - Fix conflicts; keep history tidy. +8. Fix + tests + changelog: + - Implement fixes + add/adjust tests + - Update `CHANGELOG.md` and mention `#` + `@$contrib` +9. Decide merge strategy: + - Squash (preferred): use when we want a single clean commit + - Rebase: use only when we explicitly want to preserve commit history + - If unclear, ask +10. Full gate (BEFORE commit): + - `pnpm lint && pnpm build && pnpm test` +11. Commit via committer (final merge commit only includes PR # + thanks): + - For the final merge-ready commit: `committer "fix: (#) (thanks @$contrib)" CHANGELOG.md ` + - If you need intermediate fix commits before the final merge commit, keep those messages concise and **omit** PR number/thanks. + - `land_sha=$(git rev-parse HEAD)` +12. Push updated PR branch (rebase => usually needs force): + + ```sh + git remote add prhead "$head_repo_url.git" 2>/dev/null || git remote set-url prhead "$head_repo_url.git" + git push --force-with-lease prhead HEAD:$head + ``` + +13. Merge PR (must show MERGED on GitHub): + - Squash (preferred): `gh pr merge --squash` + - Rebase (history-preserving fallback): `gh pr merge --rebase` + - Never `gh pr close` (closing is wrong) +14. Sync main: + - `git checkout main` + - `git pull --ff-only` +15. Comment on PR with what we did + SHAs + thanks: + + ```sh + merge_sha=$(gh pr view --json mergeCommit --jq '.mergeCommit.oid') + gh pr comment --body "Landed via temp rebase onto main.\n\n- Gate: pnpm lint && pnpm build && pnpm test\n- Land commit: $land_sha\n- Merge commit: $merge_sha\n\nThanks @$contrib!" + ``` + +16. Verify PR state == MERGED: + - `gh pr view --json state --jq .state` +17. Delete temp branch: + - `git branch -D temp/landpr-` diff --git a/.opencode/agents/review.md b/.opencode/agents/review.md new file mode 100644 index 0000000..61f1706 --- /dev/null +++ b/.opencode/agents/review.md @@ -0,0 +1,18 @@ +--- +description: Reviews code for quality and best practices +mode: subagent +temperature: 0.1 +tools: + write: false + edit: false + bash: false +--- + +You are in code review mode. Focus on: + +- Code quality and best practices +- Potential bugs and edge cases +- Performance implications +- Security considerations + +Provide constructive feedback without making direct changes. diff --git a/.opencode/agents/security-auditor.md b/.opencode/agents/security-auditor.md new file mode 100644 index 0000000..9f7a492 --- /dev/null +++ b/.opencode/agents/security-auditor.md @@ -0,0 +1,17 @@ +--- +description: Performs security audits and identifies vulnerabilities +mode: subagent +tools: + write: true + edit: false +--- + +You are a security expert. Focus on identifying potential security issues. + +Look for: + +- Input validation vulnerabilities +- Authentication and authorization flaws +- Data exposure risks +- Dependency vulnerabilities +- Configuration security issues diff --git a/.opencode/package.json b/.opencode/package.json new file mode 100644 index 0000000..47703c0 --- /dev/null +++ b/.opencode/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@opencode-ai/plugin": "1.2.27" + } +} diff --git a/.opencode/plugins/custom-compaction.ts b/.opencode/plugins/custom-compaction.ts new file mode 100644 index 0000000..8953408 --- /dev/null +++ b/.opencode/plugins/custom-compaction.ts @@ -0,0 +1,20 @@ +import type { Plugin } from "@opencode-ai/plugin"; + +export const CustomCompactionPlugin: Plugin = async (/* ctx */) => { + return { + "experimental.session.compacting": async (input, output) => { + // Replace the entire compaction prompt + output.prompt = ` +You are generating a continuation prompt for a multi-agent swarm session. + +Summarize: +1. The current task and its status +2. Which files are being modified and by whom +3. Any blockers or dependencies between agents +4. The next steps to complete the work + +Format as a structured prompt that a new agent can use to resume work. +`; + }, + }; +}; diff --git a/.opencode/skills/git-release/SKILL.md b/.opencode/skills/git-release/SKILL.md new file mode 100644 index 0000000..959f8de --- /dev/null +++ b/.opencode/skills/git-release/SKILL.md @@ -0,0 +1,20 @@ +--- +name: git-release +description: Create consistent releases and changelogs +license: MIT +compatibility: opencode +metadata: + audience: maintainers + workflow: github +--- + +## What I do + +- Draft release notes from merged PRs +- Propose a version bump +- Provide a copy-pasteable `gh release create` command + +## When to use me + +Use this when you are preparing a tagged release. +Ask clarifying questions if the target versioning scheme is unclear. diff --git a/.opencode/tools/project.ts b/.opencode/tools/project.ts new file mode 100644 index 0000000..5850103 --- /dev/null +++ b/.opencode/tools/project.ts @@ -0,0 +1,11 @@ +import { tool } from "@opencode-ai/plugin"; + +export default tool({ + description: "Get project information", + args: {}, + async execute(args, context) { + // Access context information + const { agent, sessionID, messageID, directory, worktree } = context; + return `Agent: ${agent}, Session: ${sessionID}, Message: ${messageID}, Directory: ${directory}, Worktree: ${worktree}`; + }, +}); diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..677e0a5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,25 @@ +# Agent TUI - Development Guide + +## Reference Documents + +- [README](README.md) - Project overview, file structure, and dev commands +- [Architecture](docs/ARCHITECTURE.md) - Architecture overview, component breakdown, and dependency flow +- [Code Style](docs/CODE_STYLE.md) - TypeScript configuration, naming conventions, class structure, and formatting guidelines + +--- + +## Framework + +This is a Bun-based TypeScript TUI application using `@opentui/core` for rendering. + +--- + +## Working with this Codebase + +1. Run `bun dev` for development with hot reload +2. Run `bun lint:fix && bun format` before committing +3. Run `bun typecheck` to verify TypeScript correctness +4. Always use `import type` for type-only imports +5. Always use `override` keyword when overriding parent methods +6. Check array bounds when using indexed access +7. Log all errors with `console.error` before fallback handling diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..5bd7ad5 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,39 @@ +# Architecture and Component Relationships + +## Architecture Overview + +The application follows a layered architecture with clear separation of concerns: + +```bash +main.ts (Entry Point) +├── AgentConfig (Configuration) +├── AgentTheme (Theme Management) +├── Renderer (Lifecycle/Singleton Manager) +│ └── createCliRenderer (@opentui/core) +└── Container (UI Composition) +├── BoxRenderable (@opentui/core) +├── InputRenderable (@opentui/core) +└── TextRenderable (@opentui/core) +``` + +## Component Breakdown + +| Component | File | Responsibility | Pattern | +| ----------- | ------------ | --------------------------------- | ----------------- | +| main.ts | Entry point | Orchestrates initialization | Procedural | +| AgentConfig | config.ts | Banner/configuration management | Simple Config | +| AgentTheme | theme.ts | Theme loading & variant selection | Strategy | +| Renderer | renderer.ts | CLI renderer lifecycle management | Singleton Manager | +| Container | container.ts | UI component composition | Composite | +| figures.ts | figures.ts | Unicode figure symbols | Utility | +| utils.ts | utils.ts | Terminal Unicode detection | Utility | + +## Dependency Flow + +```bash +@opentui/core +├── createCliRenderer() → CliRenderer +├── BoxRenderable (layout container) +├── InputRenderable (text input with events) +└── TextRenderable (static text display) +``` diff --git a/docs/CODE_STYLE.md b/docs/CODE_STYLE.md new file mode 100644 index 0000000..032662e --- /dev/null +++ b/docs/CODE_STYLE.md @@ -0,0 +1,81 @@ +# Code Style & TypeScript Guidelines + +## TypeScript Configuration + +- **Strict mode** is enabled in `tsconfig.json` +- Use `noUncheckedIndexedAccess: true` - always check array bounds +- Use `noImplicitOverride: true` - always use `override` keyword when overriding parent methods +- Use `verbatimModuleSyntax: true` - requires explicit imports/exports + +## Imports + +- External imports first, then relative imports +- Use explicit type imports with the `type` keyword when only importing types: + + ```typescript + import type { SomeType } from "./some-module"; + import { someFunction } from "some-package"; + ``` + +## Naming Conventions + +- **Classes**: PascalCase (e.g., `AgentConfig`, `Container`) +- **Interfaces**: PascalCase with `I` prefix for contracts (e.g., `IAgentConfig`, `IRenderer`) +- **Types**: PascalCase (e.g., `HexColor`, `ConfigOpts`) +- **Methods/variables**: camelCase (e.g., `getOptions()`, `themeName`) +- **Constants**: PascalCase for exported, camelCase for private + +## Class Structure + +```typescript +export class MyClass implements IMyInterface { + // Private readonly fields first + private readonly options: MyOptions; + private instance: MyInstance | null = null; + + // Constructor + constructor(options: MyOptions = {}) { + this.options = { ...options }; + } + + // Public methods + public async get(): Promise { ... } + + // Private methods + private loadSomething(): void { ... } +} +``` + +## Error Handling + +- Use try-catch blocks with `console.error` for logging +- Provide fallback values where appropriate +- Do not silently swallow errors without logging + +## Documentation + +- Use JSDoc comments for classes and public methods +- Include `@returns` and `@param` descriptions +- Keep comments concise and meaningful + +## Code Patterns + +- Use **singleton pattern** for renderer instances (see `src/renderer.ts`) +- Use **dependency injection** via constructor options +- Use **interfaces** to define contracts (e.g., `IRenderer`, `IContainer`) +- Make options readonly where immutability matters + +## Formatting (oxfmt) + +- Run `bun format` before committing +- Uses default oxfmt settings (minimal config) + +## Pre-commit Hook + +The project uses `simple-git-hooks` with a pre-commit hook that runs: + +```bash +bun lint && bun typecheck && bun run actions:up +``` + +All checks must pass before committing. diff --git a/docs/Code Patterns & Conventions.md b/docs/Code Patterns & Conventions.md new file mode 100644 index 0000000..322e7e1 --- /dev/null +++ b/docs/Code Patterns & Conventions.md @@ -0,0 +1,22 @@ +# Code Patterns & Conventions + +## Patterns Used + +1. **Singleton Pattern** - Renderer manages a single CliRenderer instance +2. **Interface-based Contracts** - IRenderer, IAgentConfig, IAgentTheme, IContainer +3. **Dependency Injection** - Options objects passed via constructor +4. **Immutable Options** - readonly fields where appropriate + +## Naming Conventions (Mostly Followed) + +- **Classes:** PascalCase (`AgentConfig`, `Container`) ✅ +- **Interfaces:** PascalCase with I prefix (`IAgentConfig`) ✅ +- **Types:** PascalCase (`ConfigOpts`, `HexColor`) ✅ +- **Methods/variables:** camelCase ✅ +- **Private fields:** private readonly ✅ + +## Import Organization + +- External imports first ✅ +- Relative imports second ✅ +- Explicit type imports where needed ✅ diff --git a/docs/Project Analysis.md b/docs/Project Analysis.md new file mode 100644 index 0000000..6ca9169 --- /dev/null +++ b/docs/Project Analysis.md @@ -0,0 +1,323 @@ +# Project Analysis + +## 1. Issues, TODOs, and Anti-Patterns + +## Critical Issues + +### 🔴 CRITICAL BUG in theme.ts (lines 71-74) + +```ts +private loadTheme(options: AgentThemeOpts): ColorTheme { + // ... + if (existsSync(THEME_PATH)) { + try { + // ... + return JSON.parse(themeFile); + } catch (error) { + console.error(...); + // BUG: This will ALWAYS throw if file doesn't exist or JSON.parse fails + return JSON.parse(readFileSync(THEME_PATH, "utf-8")); // Line 71 - Same error-prone code! + } + } + // BUG: This line is UNREACHABLE when file doesn't exist! + return options.theme ?? JSON.parse(readFileSync(THEME_PATH, "utf-8")); // Line 74 - Will throw! +} +``` + +**Problems:** + +1. If theme file doesn't exist, line 74 will throw an unhandled error +2. The catch block's fallback (line 71) re-reads the same file that may not exist +3. There's no default theme to fall back to when loading fails + +--- + +### 🔴 CRITICAL BUG in container.ts (line 104) + +```ts +remove() { + this.container.remove("body"); // BUG: Should probably be "main-container" or similar +} +``` + +The ID `"body"` doesn't match any created component ID. The container's ID is `"main-container"`. + +--- + +### 🔴 BUG in container.ts (line 50) + +```ts +const queryPrefix = new BoxRenderable(this.renderer, { + id: "query-prefix", + backgroundColor: _theme.seeds.primary, +}); // Missing semicolon! +``` + +This is syntactically invalid - it's a variable declaration without a statement terminator before the next `const`. + +--- + +## High Priority Issues + +### 🟠 Missing error handling in main.ts + +```ts +async function main() { + // If any of these throw, the error is caught but resources may leak + const rendererManager = new Renderer(); + const renderer = await rendererManager.get(); // Not wrapped in try-catch + // ... +} +main().catch(console.error); // Only logs, no cleanup +``` + +**Problems:** + +- No try-catch around initialization +- If renderer creation fails, no cleanup +- Global `console.error` is used instead of structured logging + +--- + +### 🟠 No cleanup in main.ts + +The `Renderer.destroy()` is never called. If the application exits unexpectedly, resources may not be properly cleaned up. + +--- + +### 🟠 figures.ts line 287 uses any type + +```ts +export const replaceSymbols = (text: string, { useFallback = !shouldUseMain } = {}) => { + if (useFallback) { + for (const [key, mainSymbol] of replacements) { + text = text.replaceAll(mainSymbol, (fallbackSymbols as any)[key]); // BAD! + } + } + return text; +}; +``` + +This defeats type safety. The cast should be to a proper type. + +--- + +### 🟠 Duplicate type definitions in renderer.ts + +```ts +export type CliRenderer = Awaited>; +export type CliRendererOptions = Parameters[0]; +export type RendererOpts = Parameters[0]; // DUPLICATE of CliRendererOptions! +``` + +`CliRendererOptions` and `RendererOpts` are identical. + +--- + +## Medium Priority Issues + +### 🟡 Misleading JSDoc comments + +In `config.ts`: + +```ts +/** + * Defines the contract for a renderer provider. + * It's responsible for the lifecycle of the renderer instance. + */ +export interface IAgentConfig { ... } +``` + +The comment says "renderer provider" but this is actually about config, not rendering. Copy-paste error from `renderer.ts`. + +--- + +### 🟡 Missing interface implementations + +`IContainer.remove()` is defined but has incorrect implementation (see bug above). + +--- + +### 🟡 No validation of theme loading result + +In `theme.ts`, after `JSON.parse()`, there's no validation that the parsed object conforms to `ColorTheme` interface. Malformed JSON could cause runtime errors later. + +--- + +### 🟡 Unused shouldUseMain variable scope issue + +```ts +const shouldUseMain = isUnicodeSupported(); +const figures = shouldUseMain ? mainSymbols : fallbackSymbols; +export default figures; +``` + +The `shouldUseMain` is evaluated at module load time. If the terminal environment changes after load, the wrong figures might be returned. + +--- + +## Low Priority Issues + +### 🔵 Inconsistent error handling + +- `theme.ts`: Uses `console.error` with context +- `main.ts`: Uses bare `console.error` (via `.catch(console.error)`) +- No structured logging + +--- + +### 🔵 No tests + +The CI workflow references `bun test` but no test files exist. The test job will fail. + +--- + +### 🔵 Empty .vscode/settings.json + +```json +{} +``` + +The workspace settings are empty, meaning no project-specific VSCode configuration. + +--- + +### 🔵 Theme files are identical copies + +`dracula.json` and `default.json` have identical content. This appears to be a copy-paste mistake rather than actual Dracula theme values. + +--- + +## Code Smell: Stale Comments + +In `config.ts`: + +```ts +/** + * Manages the lifecycle of the CLI renderer instance. // WRONG - this is Config, not Renderer + * This class follows the singleton pattern for the renderer instance, // WRONG + */ +export class AgentConfig implements IAgentConfig { ... } +``` + +The comments are copy-pasted from Renderer and are completely inaccurate. + +--- + +## 2. Dependency on @opentui/core + +## How it's used + +| Component | Usage | Purpose | +| ------------ | ------------------- | ------------------------------------- | +| renderer.ts | createCliRenderer() | Creates the CLI rendering context | +| container.ts | BoxRenderable | Layout container for UI elements | +| container.ts | InputRenderable | Text input with keyboard/paste events | +| container.ts | TextRenderable | Static text display | + +--- + +## Key Observations + +1. Version pinned: `^0.1.83` - This is good for stability +2. Single source of truth: Types are derived from `@opentui/core` in `renderer.ts`: + + ```ts + export type CliRenderer = Awaited>; + export type RendererOpts = Parameters[0]; + ``` + +3. Renderer singleton: The `Renderer` class wraps `createCliRenderer()` to ensure single instance +4. Not all API used: Only 3 renderable types are used (`BoxRenderable`, `InputRenderable`, `TextRenderable`) - likely more exist in the library + +--- + +## Risks + +- No local type definitions: The app relies entirely on `@opentui/core` types +- Version compatibility: If `@opentui/core` changes its API, this app could break without notice +- API surface not validated: `InputRenderable.blur()` method is used but not verified to exist + +--- + +## 3. Missing Features and Incomplete Implementations + +## Missing Features + +1. **No actual agent functionality** + - The app name is `"agent-tui"` but there's no agent implementation + - `InputRenderable` is set up but input is only logged to console + - No message sending, AI integration, or response handling + +2. **No theme switching at runtime** + - Themes are loaded at startup + - No UI to switch between themes (`dracula`, `nord`, `nightowl`, `default` are all loaded but never selectable) + +3. **No command handling** + - Input only logs key presses + - No command parsing or execution + +4. **No response display area** + - The UI has no area to show agent responses + +5. **No history/message list** + - No conversation history component + +6. **No settings panel** + - `AgentConfig` only has a banner option + +--- + +## Incomplete Implementations + +1. **Container remove() method** + - Doesn't work correctly (wrong ID) + +2. **Theme fallback mechanism** + - Has bugs that prevent graceful fallback + +3. **figures.ts module** + - Has Unicode detection but it's not integrated into the UI + - `replaceSymbols()` function exists but is never called + +--- + +## Project Setup Issues + +1. **README is outdated** + - Says `bun run index.ts` but entry point is `main.ts` + +2. **No test framework configured** + - `bun test` is referenced in CI but no tests exist + - `AGENTS.md` mentions "no test framework configured yet" + +3. **No .env or environment configuration** + - No way to configure API keys or endpoints for agent integration + +--- + +## Summary Table + +| Category | Count | Severity | +| ---------------- | ----- | -------- | +| Critical Bugs | 3 | 🔴 | +| High Priority | 4 | 🟠 | +| Medium Priority | 4 | 🟡 | +| Low Priority | 5 | 🔵 | +| Missing Features | 6 | - | +| Incomplete Impl | 3 | - | + +--- + +## Recommendations + +1. Fix critical bugs immediately - especially the theme loading and syntax error +2. Add proper error handling with try-catch blocks and fallback values +3. Implement actual agent functionality or rename the project +4. Add tests before adding more features +5. Update README to reflect actual entry point +6. Remove duplicate theme files or differentiate them +7. Add runtime theme switching UI +8. Add proper logging instead of `console.error` +9. Fix the `remove()` method or remove it if not needed +10. Consider adding a config file for API keys and settings diff --git a/opencode.json b/opencode.json new file mode 100644 index 0000000..1898f9d --- /dev/null +++ b/opencode.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://opencode.ai/config.json", + "plugin": [ + "@franlol/opencode-md-table-formatter@latest", + "opencode-helicone-session", + "opencode-pty", + "opencode-vibeguard", + "opencode-wakatime" + ], + "permission": { + "skill": { + "*": "allow", + "pr-review": "allow", + "internal-*": "deny", + "experimental-*": "ask" + } + }, + "mcp": {}, + "lsp": {}, + "agent": { + "creative": { + "color": "#ff6b6b" + }, + "code-reviewer": { + "color": "accent" + }, + "orchestrator": { + "mode": "primary", + "permission": { + "task": { + "*": "deny", + "orchestrator-*": "deny", + "code-reviewer": "ask" + } + } + } + } +} From e602cc23ebf065869f5a940a273f4c7f7e41d327 Mon Sep 17 00:00:00 2001 From: Joydip Roy Date: Sat, 21 Mar 2026 13:18:44 +0530 Subject: [PATCH 3/6] fix: add bun/node types to tsconfig and fix lib entry --- bun.lock | 5 ++++- package.json | 3 ++- tsconfig.json | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/bun.lock b/bun.lock index bfa6d6c..06a07cb 100644 --- a/bun.lock +++ b/bun.lock @@ -9,6 +9,7 @@ }, "devDependencies": { "@types/bun": "latest", + "@types/node": "^25.5.0", "@typescript/native-preview": "^7.0.0-dev.20260320.1", "actions-up": "^1.12.0", "oxfmt": "^0.35.0", @@ -173,7 +174,7 @@ "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], - "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], + "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260320.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260320.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260320.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260320.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260320.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260320.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260320.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260320.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-uLkkdYwvJQ/atPqMW9RUt1j71lcbL53e7a+G/I3AXb5QioMdWkG6nbNfKPM+06RgXwPDyDV9+bI0FT/13BEVgg=="], @@ -331,6 +332,8 @@ "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "bun-types/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], + "image-q/@types/node": ["@types/node@16.9.1", "", {}, "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="], "pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], diff --git a/package.json b/package.json index d499d28..d3a8d04 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "prepare": "simple-git-hooks", "lint": "oxlint .", "lint:fix": "oxlint --fix .", - "format": "oxfmt .", + "format": "oxfmt", "actions:up": "actions-up", "typecheck": "tsgo --noEmit", "dev": "bun --watch main.ts", @@ -23,6 +23,7 @@ }, "devDependencies": { "@types/bun": "latest", + "@types/node": "^25.5.0", "@typescript/native-preview": "^7.0.0-dev.20260320.1", "actions-up": "^1.12.0", "oxfmt": "^0.35.0", diff --git a/tsconfig.json b/tsconfig.json index bfa0fea..eac9c02 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,7 @@ // Best practices "strict": true, "skipLibCheck": true, + "types": ["bun", "node"], "noFallthroughCasesInSwitch": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, From 7890cd7db734fdad982ffbece4fcf6e6ae034802 Mon Sep 17 00:00:00 2001 From: Joydip Roy Date: Sat, 21 Mar 2026 13:25:57 +0530 Subject: [PATCH 4/6] test: add unit tests for AgentTheme, AgentConfig, utils, and figures --- test/config.test.ts | 35 +++++++++++ test/figures.test.ts | 65 +++++++++++++++++++ test/theme.test.ts | 145 +++++++++++++++++++++++++++++++++++++++++++ test/utils.test.ts | 13 ++++ 4 files changed, 258 insertions(+) create mode 100644 test/config.test.ts create mode 100644 test/figures.test.ts create mode 100644 test/theme.test.ts create mode 100644 test/utils.test.ts diff --git a/test/config.test.ts b/test/config.test.ts new file mode 100644 index 0000000..0a6a6be --- /dev/null +++ b/test/config.test.ts @@ -0,0 +1,35 @@ +import { describe, test, expect } from "bun:test"; +import { AgentConfig } from "../src/config"; + +describe("AgentConfig", () => { + describe("constructor", () => { + test("defaults to empty options", () => { + const config = new AgentConfig(); + expect(config.getOptions()).toEqual({}); + }); + + test("stores provided banner", () => { + const config = new AgentConfig({ banner: "my banner" }); + expect(config.getOptions()).toEqual({ banner: "my banner" }); + }); + + test("stores arbitrary options", () => { + const config = new AgentConfig({ banner: "test" }); + expect(config.getOptions()).toEqual({ banner: "test" }); + }); + }); + + describe("getBanner()", () => { + test("returns custom banner when provided", () => { + const customBanner = ">>> CUSTOM BANNER <<<"; + const config = new AgentConfig({ banner: customBanner }); + expect(config.getBanner()).toBe(customBanner); + }); + + test("returns default banner when not provided", () => { + const config = new AgentConfig(); + const banner = config.getBanner(); + expect(banner).toContain("█████"); + }); + }); +}); diff --git a/test/figures.test.ts b/test/figures.test.ts new file mode 100644 index 0000000..a079a81 --- /dev/null +++ b/test/figures.test.ts @@ -0,0 +1,65 @@ +import { describe, test, expect } from "bun:test"; +import figures, { mainSymbols, fallbackSymbols, replaceSymbols } from "../src/figures"; + +describe("figures", () => { + test("figures default export is an object", () => { + expect(typeof figures).toBe("object"); + expect(figures).not.toBeNull(); + }); + + test("figures has expected keys", () => { + expect(figures).toHaveProperty("tick"); + expect(figures).toHaveProperty("cross"); + expect(figures).toHaveProperty("info"); + expect(figures).toHaveProperty("warning"); + expect(figures).toHaveProperty("ellipsis"); + expect(figures).toHaveProperty("pointer"); + }); + + test("mainSymbols and fallbackSymbols are objects", () => { + expect(typeof mainSymbols).toBe("object"); + expect(typeof fallbackSymbols).toBe("object"); + }); + + test("mainSymbols has all required figure keys", () => { + const requiredKeys = [ + "tick", + "cross", + "info", + "warning", + "ellipsis", + "pointer", + "arrowUp", + "arrowDown", + "lineVertical", + "star", + "checkboxOn", + "checkboxOff", + ]; + for (const key of requiredKeys) { + expect(mainSymbols).toHaveProperty(key); + } + }); +}); + +describe("replaceSymbols", () => { + test("returns text unchanged when useFallback is false", () => { + const result = replaceSymbols("✔ ✘ ℹ ⚠", { useFallback: false }); + expect(result).toBe("✔ ✘ ℹ ⚠"); + }); + + test("replaces main symbols with fallbacks when useFallback is true", () => { + const result = replaceSymbols("✔ ✘ ℹ ⚠", { useFallback: true }); + expect(result).toBe("√ × i ‼"); + }); + + test("replaces multiple occurrences of same symbol", () => { + const result = replaceSymbols("✔ ✔ ✔", { useFallback: true }); + expect(result).toBe("√ √ √"); + }); + + test("returns original text with unknown symbols unchanged", () => { + const result = replaceSymbols("unknown text", { useFallback: true }); + expect(result).toBe("unknown text"); + }); +}); diff --git a/test/theme.test.ts b/test/theme.test.ts new file mode 100644 index 0000000..cd539ae --- /dev/null +++ b/test/theme.test.ts @@ -0,0 +1,145 @@ +import { describe, test, expect, beforeEach, afterEach } from "bun:test"; +import { AgentTheme, type ColorTheme, type ThemeVariant } from "../src/theme"; + +const VALID_THEME: ColorTheme = { + name: "Test Theme", + id: "test-theme", + light: { + seeds: { + neutral: "#ffffff", + primary: "#7c6bf5", + success: "#2fbf71", + warning: "#f7a14d", + error: "#d9536f", + info: "#1d7fc5", + interactive: "#7c6bf5", + diffAdd: "#9fe3b3", + diffDelete: "#f8a1b8", + }, + }, + dark: { + seeds: { + neutral: "#1d1e28", + primary: "#bd93f9", + success: "#50fa7b", + warning: "#ffb86c", + error: "#ff5555", + info: "#8be9fd", + interactive: "#bd93f9", + diffAdd: "#2fb27d", + diffDelete: "#ff6b81", + }, + }, +}; + +const MALFORMED_THEMES = { + missingSeeds: { + name: "Bad", + id: "bad", + light: {}, + dark: {}, + } as unknown as ColorTheme, + missingFields: { + name: "Bad", + id: "bad", + } as unknown as ColorTheme, + invalidHex: { + name: "Bad", + id: "bad", + light: { + seeds: { + neutral: "#ffffff", + primary: "not-a-color", + success: "#2fbf71", + warning: "#f7a14d", + error: "#d9536f", + info: "#1d7fc5", + interactive: "#7c6bf5", + diffAdd: "#9fe3b3", + diffDelete: "#f8a1b8", + }, + }, + dark: VALID_THEME.dark, + } as unknown as ColorTheme, + missingDarkVariant: { + name: "Light Only", + id: "light-only", + light: VALID_THEME.light, + dark: {} as ThemeVariant, + } as unknown as ColorTheme, +}; + +describe("AgentTheme", () => { + let origError: typeof console.error; + beforeEach(() => { + origError = console.error; + console.error = () => {}; + }); + afterEach(() => { + console.error = origError; + }); + + describe("constructor", () => { + test("defaults to empty options", () => { + const theme = new AgentTheme(); + expect(theme.getOptions()).toEqual({}); + }); + + test("stores provided options", () => { + const opts = { mode: "dark" as const, themeName: "nord" }; + const theme = new AgentTheme(opts); + expect(theme.getOptions()).toEqual(opts); + }); + }); + + describe("getTheme()", () => { + test("returns light variant by default", () => { + const theme = new AgentTheme({ theme: VALID_THEME }); + expect(theme.getTheme()).toEqual(VALID_THEME.light); + }); + + test("returns dark variant when mode is dark", () => { + const theme = new AgentTheme({ theme: VALID_THEME, mode: "dark" }); + expect(theme.getTheme()).toEqual(VALID_THEME.dark); + }); + + test("falls back to light if requested mode is missing", () => { + const theme = new AgentTheme({ theme: VALID_THEME, mode: "dark" }); + expect(theme.getTheme()).toEqual(VALID_THEME.dark); + }); + }); + + describe("getThemeMetadata()", () => { + test("returns name and id", () => { + const theme = new AgentTheme({ theme: VALID_THEME }); + expect(theme.getThemeMetadata()).toEqual({ name: "Test Theme", id: "test-theme" }); + }); + }); + + describe("validateTheme", () => { + test("accepts a valid theme with all seed colors", () => { + const theme = new AgentTheme({ theme: VALID_THEME }); + expect(theme.getThemeMetadata().id).toBe("test-theme"); + }); + + test("rejects theme missing required top-level fields", () => { + const theme = new AgentTheme({ theme: MALFORMED_THEMES.missingFields }); + expect(theme.getThemeMetadata().id).toBe("default"); + }); + + test("rejects theme with variants missing seeds", () => { + const theme = new AgentTheme({ theme: MALFORMED_THEMES.missingSeeds }); + expect(theme.getThemeMetadata().id).toBe("default"); + }); + + test("rejects theme with invalid hex color in seeds", () => { + const theme = new AgentTheme({ theme: MALFORMED_THEMES.invalidHex }); + expect(theme.getThemeMetadata().id).toBe("default"); + }); + + test("rejects theme with empty dark variant", () => { + const theme = new AgentTheme({ theme: MALFORMED_THEMES.missingDarkVariant }); + expect(theme.getThemeMetadata().id).toBe("default"); + }); + }); +}); diff --git a/test/utils.test.ts b/test/utils.test.ts new file mode 100644 index 0000000..20df2fb --- /dev/null +++ b/test/utils.test.ts @@ -0,0 +1,13 @@ +import { describe, test, expect } from "bun:test"; +import isUnicodeSupported from "../src/utils"; + +describe("isUnicodeSupported", () => { + test("is a function", () => { + expect(typeof isUnicodeSupported).toBe("function"); + }); + + test("returns a boolean", () => { + const result = isUnicodeSupported(); + expect(typeof result).toBe("boolean"); + }); +}); From 5ddaee014c3e9d70b37d053a6e8d9b6388ec8962 Mon Sep 17 00:00:00 2001 From: Joydip Roy Date: Mon, 30 Mar 2026 09:20:18 +0530 Subject: [PATCH 5/6] chore: TUI layout improved --- .github/workflows/autofix.yml | 4 +- .github/workflows/ci.yml | 4 +- opencode.json => .opencode/opencode.json | 27 +-- .opencode/package.json | 2 +- .opencode/skills/git-release/SKILL.md | 20 -- bun.lock | 70 +++---- main.ts | 50 +++-- package.json | 8 +- src/cli.ts | 116 +++++++++++ src/command-palette.ts | 202 ++++++++++++++++++ src/connection-manager.ts | 148 ++++++++++++++ src/container.ts | 247 +++++++++++++++++++---- src/errors.ts | 55 +++++ src/llm-client.ts | 186 +++++++++++++++++ src/llm-types.ts | 84 ++++++++ src/message-list.ts | 109 ++++++++++ src/persistent-config.ts | 69 +++++++ src/renderer.ts | 34 +++- src/status-bar.ts | 187 +++++++++++++++++ src/store.ts | 101 +++++++++ src/theme.ts | 7 +- src/types.ts | 54 +++++ src/typing-indicator.ts | 87 ++++++++ 23 files changed, 1736 insertions(+), 135 deletions(-) rename opencode.json => .opencode/opencode.json (53%) delete mode 100644 .opencode/skills/git-release/SKILL.md create mode 100644 src/cli.ts create mode 100644 src/command-palette.ts create mode 100644 src/connection-manager.ts create mode 100644 src/errors.ts create mode 100644 src/llm-client.ts create mode 100644 src/llm-types.ts create mode 100644 src/message-list.ts create mode 100644 src/persistent-config.ts create mode 100644 src/status-bar.ts create mode 100644 src/store.ts create mode 100644 src/types.ts create mode 100644 src/typing-indicator.ts diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index abb3011..ae19b83 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -9,12 +9,12 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Bun - uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: package.json - name: Setup Biome - uses: biomejs/setup-biome@29711cbb52afee00eb13aeb30636592f9edc0088 # v2.7.0 + uses: biomejs/setup-biome@4c91541eaada48f67d7dbd7833600ce162b68f51 # v2.7.1 with: version: latest diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b687f95..1b746ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Bun - uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: package.json @@ -36,7 +36,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Bun - uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: package.json diff --git a/opencode.json b/.opencode/opencode.json similarity index 53% rename from opencode.json rename to .opencode/opencode.json index 1898f9d..d80a17f 100644 --- a/opencode.json +++ b/.opencode/opencode.json @@ -15,24 +15,15 @@ "experimental-*": "ask" } }, - "mcp": {}, - "lsp": {}, - "agent": { - "creative": { - "color": "#ff6b6b" + "mcp": { + "browser-mcp": { + "type": "local", + "command": ["bunx", "-y", "@browsermcp/mcp@latest"] }, - "code-reviewer": { - "color": "accent" - }, - "orchestrator": { - "mode": "primary", - "permission": { - "task": { - "*": "deny", - "orchestrator-*": "deny", - "code-reviewer": "ask" - } - } + "sqlite-mcp": { + "type": "local", + "command": ["bunx", "-y", "sqlite-mcp-server"] } - } + }, + "lsp": {} } diff --git a/.opencode/package.json b/.opencode/package.json index 47703c0..4b8b4cb 100644 --- a/.opencode/package.json +++ b/.opencode/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "@opencode-ai/plugin": "1.2.27" + "@opencode-ai/plugin": "1.3.6" } } diff --git a/.opencode/skills/git-release/SKILL.md b/.opencode/skills/git-release/SKILL.md deleted file mode 100644 index 959f8de..0000000 --- a/.opencode/skills/git-release/SKILL.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: git-release -description: Create consistent releases and changelogs -license: MIT -compatibility: opencode -metadata: - audience: maintainers - workflow: github ---- - -## What I do - -- Draft release notes from merged PRs -- Propose a version bump -- Provide a copy-pasteable `gh release create` command - -## When to use me - -Use this when you are preparing a tagged release. -Ask clarifying questions if the target versioning scheme is unclear. diff --git a/bun.lock b/bun.lock index 06a07cb..0821149 100644 --- a/bun.lock +++ b/bun.lock @@ -80,19 +80,19 @@ "@jimp/utils": ["@jimp/utils@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "tinycolor2": "^1.6.0" } }, "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA=="], - "@opentui/core": ["@opentui/core@0.1.90", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.90", "@opentui/core-darwin-x64": "0.1.90", "@opentui/core-linux-arm64": "0.1.90", "@opentui/core-linux-x64": "0.1.90", "@opentui/core-win32-arm64": "0.1.90", "@opentui/core-win32-x64": "0.1.90", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-Os2dviqWVETU3kaK36lbSvdcI93GAWhw0xb9ng/d0DWYuM9scRmAhLHiOayp61saWv/BR8OJXeuQYHvrp5rd6A=="], + "@opentui/core": ["@opentui/core@0.1.92", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.92", "@opentui/core-darwin-x64": "0.1.92", "@opentui/core-linux-arm64": "0.1.92", "@opentui/core-linux-x64": "0.1.92", "@opentui/core-win32-arm64": "0.1.92", "@opentui/core-win32-x64": "0.1.92", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-c+KdYAIH3M8n24RYaor+t7AQtKZ3l84L7xdP7DEaN4xtuYH8W08E6Gi+wUal4g+HSai3HS9irox68yFf0VPAxw=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.90", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XFrm2zCg1SlHPQ5A2HX/I4dCrmTjYaCJIIpo3QuPIvZBGH3aBMdWDJh2tXw7AB5Mmh8X1K4hDkP5nlK9x0Ewow=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.92", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NX/qFRuc7My0pazyOrw9fdTXmU7omXcZzQuHcsaVnwssljaT52UYMrJ7mCKhSo69RhHw0lnGCymTorvz3XBdsA=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.90", "", { "os": "darwin", "cpu": "x64" }, "sha512-vbDpUsnlZ+0CeVKyBBXE+l2+X1XoVncMxMOhXTiMtud2/Cwu+Vfs/g3LC/6Zv08yaytA+9g7Z8sdf0QCqFyQ4w=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.92", "", { "os": "darwin", "cpu": "x64" }, "sha512-Zb4jn33hOf167llINKLniOabQIycs14LPOBZnQ6l4khbeeTPVJdG8gy9PhlAyIQygDKmRTFncVlP0RP+L6C7og=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.90", "", { "os": "linux", "cpu": "arm64" }, "sha512-OTbvBTP5mVQ4uwKyuz6b59ElG+D0i1Ln+q6cVhNkLgeRLySIn1uXEzUFQGlnVgb8lFDANsn3yQmdv+R+Cpw0og=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.92", "", { "os": "linux", "cpu": "arm64" }, "sha512-4VA1A91OTMPJ3LkAyaxKEZVJsk5jIc3Kz0gV2vip8p2aGLPpYHHpkFZpXP/FyzsnJzoSGftBeA6ya1GKa5bkXg=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.90", "", { "os": "linux", "cpu": "x64" }, "sha512-2PJi/LLlO7tGk9Ful/n+6iBdg1RFrA9ibU7wVneE6Z1P0LCYeu7bpwMzea1TXL0eAQWPHsjTs9aPlqPxln0EJw=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.92", "", { "os": "linux", "cpu": "x64" }, "sha512-tr7va8hfKS1uY+TBmulQBoBlwijzJk56K/U/L9/tbHfW7oJctqxPVwEFHIh1HDcOQ3/UhMMWGvMfeG6cFiK8/A=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.90", "", { "os": "win32", "cpu": "arm64" }, "sha512-+sTRaOb7gCMZ6iLuuG4y9kzyweJzBDcIJN0Xh49ikFWTwVECDXEVtXahNGlw57avm2yYUoNzmpBjK/LV7zBj9A=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.92", "", { "os": "win32", "cpu": "arm64" }, "sha512-34YM3uPtDjzUVeSnJWIK2J8mxyduzV7f3mYc4Hub0glNpUdM1jjzF2HvvvnrKK5ElzTsIcno3c3lOYT8yvG1Zg=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.90", "", { "os": "win32", "cpu": "x64" }, "sha512-aVFyErckWp4oW9NJ/ZDKBUAlTlfVUiRXGP63JXFOoeqI7EYaM8uBt6rgZAJuUdFWCN2Q66WRS8Y2mk+0BJwVBg=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.92", "", { "os": "win32", "cpu": "x64" }, "sha512-uk442kA2Vn0mmJHHqk5sPM+Zai/AN9sgl7egekhoEOUx2VK3gxftKsVlx2YVpCHTvTE/S+vnD2WpQaJk2SNjww=="], "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.35.0", "", { "os": "android", "cpu": "arm" }, "sha512-BaRKlM3DyG81y/xWTsE6gZiv89F/3pHe2BqX2H4JbiB8HNVlWWtplzgATAE5IDSdwChdeuWLDTQzJ92Lglw3ZA=="], @@ -132,43 +132,43 @@ "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.35.0", "", { "os": "win32", "cpu": "x64" }, "sha512-WCDJjlS95NboR0ugI2BEwzt1tYvRDorDRM9Lvctls1SLyKYuNRCyrPwp1urUPFBnwgBNn9p2/gnmo7gFMySRoQ=="], - "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-IyfYPthZyiSKwAv/dLjeO18SaK8MxLI9Yss2JrRDyweQAkuL3LhEy7pwIwI7uA3KQc1Vdn20kdmj3q0oUIQL6A=="], + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.57.0", "", { "os": "android", "cpu": "arm" }, "sha512-C7EiyfAJG4B70496eV543nKiq5cH0o/xIh/ufbjQz3SIvHhlDDsyn+mRFh+aW8KskTyUpyH2LGWL8p2oN6bl1A=="], - "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.56.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Ga5zYrzH6vc/VFxhn6MmyUnYEfy9vRpwTIks99mY3j6Nz30yYpIkWryI0QKPCgvGUtDSXVLEaMum5nA+WrNOSg=="], + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.57.0", "", { "os": "android", "cpu": "arm64" }, "sha512-9i80AresjZ/FZf5xK8tKFbhQnijD4s1eOZw6/FHUwD59HEZbVLRc2C88ADYJfLZrF5XofWDiRX/Ja9KefCLy7w=="], - "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.56.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ogmbdJysnw/D4bDcpf1sPLpFThZ48lYp4aKYm10Z/6Nh1SON6NtnNhTNOlhEY296tDFItsZUz+2tgcSYqh8Eyw=="], + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.57.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0eUfhRz5L2yKa9I8k3qpyl37XK3oBS5BvrgdVIx599WZK63P8sMbg+0s4IuxmIiZuBK68Ek+Z+gcKgeYf0otsg=="], - "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.56.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-x8QE1h+RAtQ2g+3KPsP6Fk/tdz6zJQUv5c7fTrJxXV3GHOo+Ry5p/PsogU4U+iUZg0rj6hS+E4xi+mnwwlDCWQ=="], + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.57.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-UvrSuzBaYOue+QMAcuDITe0k/Vhj6KZGjfnI6x+NkxBTke/VoM7ZisaxgNY0LWuBkTnd1OmeQfEQdQ48fRjkQg=="], - "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.56.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6G+WMZvwJpMvY7my+/SHEjb7BTk/PFbePqLpmVmUJRIsJMy/UlyYqjpuh0RCgYYkPLcnXm1rUM04kbTk8yS1Yg=="], + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.57.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-wtQq0dCoiw4bUwlsNVDJJ3pxJA218fOezpgtLKrbQqUtQJcM9yP8z+I9fu14aHg0uyAxIY+99toL6uBa2r7nxA=="], - "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-YYHBsk/sl7fYwQOok+6W5lBPeUEvisznV/HZD2IfZmF3Bns6cPC3Z0vCtSEOaAWTjYWN3jVsdu55jMxKlsdlhg=="], + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.57.0", "", { "os": "linux", "cpu": "arm" }, "sha512-qxFWl2BBBFcT4djKa+OtMdnLgoHEJXpqjyGwz8OhW35ImoCwR5qtAGqApNYce5260FQqoAHW8S8eZTjiX67Tsg=="], - "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+AZK8rOUr78y8WT6XkDb04IbMRqauNV+vgT6f8ZLOH8wnpQ9i7Nol0XLxAu+Cq7Sb+J9wC0j6Km5hG8rj47/yQ=="], + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.57.0", "", { "os": "linux", "cpu": "arm" }, "sha512-SQoIsBU7J0bDW15/f0/RvxHfY3Y0+eB/caKBQtNFbuerTiA6JCYx9P1MrrFTwY2dTm/lMgTSgskvCEYk2AtG/Q=="], - "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-urse2SnugwJRojUkGSSeH2LPMaje5Q50yQtvtL9HFckiyeqXzoFwOAZqD5TR29R2lq7UHidfFDM9EGcchcbb8A=="], + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.57.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jqxYd1W6WMeozsCmqe9Rzbu3SRrGTyGDAipRlRggetyYbUksJqJKvUNTQtZR/KFoJPb+grnSm5SHhdWrywv3RQ=="], - "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-rkTZkBfJ4TYLjansjSzL6mgZOdN5IvUnSq3oNJSLwBcNvy3dlgQtpHPrRxrCEbbcp7oQ6If0tkNaqfOsphYZ9g=="], + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.57.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-i66WyEPVEvq9bxRUCJ/MP5EBfnTDN3nhwEdFZFTO5MmLLvzngfWEG3NSdXQzTT3vk5B9i6C2XSIYBh+aG6uqyg=="], - "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-uqL1kMH3u69/e1CH2EJhP3CP28jw2ExLsku4o8RVAZ7fySo9zOyI2fy9pVlTAp4voBLVgzndXi3SgtdyCTa2aA=="], + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.57.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-oMZDCwz4NobclZU3pH+V1/upVlJZiZvne4jQP+zhJwt+lmio4XXr4qG47CehvrW1Lx2YZiIHuxM2D4YpkG3KVA=="], - "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-j0CcMBOgV6KsRaBdsebIeiy7hCjEvq2KdEsiULf2LZqAq0v1M1lWjelhCV57LxsqaIGChXFuFJ0RiFrSRHPhSg=="], + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-uoBnjJ3MMEBbfnWC1jSFr7/nSCkcQYa72NYoNtLl1imshDnWSolYCjzb8LVCwYCCfLJXD+0gBLD7fyC14c0+0g=="], - "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-7VDOiL8cDG3DQ/CY3yKjbV1c4YPvc4vH8qW09Vv+5ukq3l/Kcyr6XGCd5NvxUmxqDb2vjMpM+eW/4JrEEsUetA=="], + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-BdrwD7haPZ8a9KrZhKJRSj6jwCor+Z8tHFZ3PT89Y3Jq5v3LfMfEePeAmD0LOTWpiTmzSzdmyw9ijneapiVHKQ=="], - "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.56.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-JGRpX0M+ikD3WpwJ7vKcHKV6Kg0dT52BW2Eu2BupXotYeqGXBrbY+QPkAyKO6MNgKozyTNaRh3r7g+VWgyAQYQ=="], + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.57.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-BNs+7ZNsRstVg2tpNxAXfMX/Iv5oZh204dVyb8Z37+/gCh+yZqNTlg6YwCLIMPSk5wLWIGOaQjT0GUOahKYImw=="], - "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-dNaICPvtmuxFP/VbqdofrLqdS3bM/AKJN3LMJD52si44ea7Be1cBk6NpfIahaysG9Uo+L98QKddU9CD5L8UHnQ=="], + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.57.0", "", { "os": "linux", "cpu": "x64" }, "sha512-AghS18w+XcENcAX0+BQGLiqjpqpaxKJa4cWWP0OWNLacs27vHBxu7TYkv9LUSGe5w8lOJHeMxcYfZNOAPqw2bg=="], - "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pF1vOtM+GuXmbklM1hV8WMsn6tCNPvkUzklj/Ej98JhlanbmA2RB1BILgOpwSuCTRTIYx2MXssmEyQQ90QF5aA=="], + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.57.0", "", { "os": "linux", "cpu": "x64" }, "sha512-E/FV3GB8phu/Rpkhz5T96hAiJlGzn91qX5yj5gU754P5cmVGXY1Jw/VSjDSlZBCY3VHjsVLdzgdkJaomEmcNOg=="], - "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.56.0", "", { "os": "none", "cpu": "arm64" }, "sha512-bp8NQ4RE6fDIFLa4bdBiOA+TAvkNkg+rslR+AvvjlLTYXLy9/uKAYLQudaQouWihLD/hgkrXIKKzXi5IXOewwg=="], + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.57.0", "", { "os": "none", "cpu": "arm64" }, "sha512-xvZ2yZt0nUVfU14iuGv3V25jpr9pov5N0Wr28RXnHFxHCRxNDMtYPHV61gGLhN9IlXM96gI4pyYpLSJC5ClLCQ=="], - "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.56.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-PxT4OJDfMOQBzo3OlzFb9gkoSD+n8qSBxyVq2wQSZIHFQYGEqIRTo9M0ZStvZm5fdhMqaVYpOnJvH2hUMEDk/g=="], + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.57.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z4D8Pd0AyHBKeazhdIXeUUy5sIS3Mo0veOlzlDECg6PhRRKgEsBJCCV1n+keUZtQ04OP+i7+itS3kOykUyNhDg=="], - "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.56.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-PTRy6sIEPqy2x8PTP1baBNReN/BNEFmde0L+mYeHmjXE1Vlcc9+I5nsqENsB2yAm5wLkzPoTNCMY/7AnabT4/A=="], + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.57.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-StOZ9nFMVKvevicbQfql6Pouu9pgbeQnu60Fvhz2S6yfMaii+wnueLnqQ5I1JPgNF0Syew4voBlAaHD13wH6tw=="], - "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZHa0clocjLmIDr+1LwoWtxRcoYniAvERotvwKUYKhH41NVfl0Y4LNbyQkwMZzwDvKklKGvGZ5+DAG58/Ik47tQ=="], + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.57.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6PuxhYgth8TuW0+ABPOIkGdBYw+qYGxgIdXPHSVpiCDm+hqTTWCmC739St1Xni0DJBt8HnSHTG67i1y6gr8qrA=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], @@ -176,21 +176,21 @@ "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], - "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260320.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260320.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260320.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260320.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260320.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260320.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260320.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260320.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-uLkkdYwvJQ/atPqMW9RUt1j71lcbL53e7a+G/I3AXb5QioMdWkG6nbNfKPM+06RgXwPDyDV9+bI0FT/13BEVgg=="], + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260329.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260329.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260329.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260329.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260329.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260329.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260329.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260329.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-v5lJ0TgSt2m9yVk2xoj9+NH/gTDeWTLaWGPx6MJsUKOYd6bmCJhHbMcWmb8d/zlfhE9ffpixUKYj62CdYfriqA=="], - "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260320.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9zadqk/wJ+4bCheapoP/wZZxIGbHotKj+QDvwlCeB93LFrcuIYDt8q91xW1tzrc12YrUTgWXqJuYHTNZ9fzyHw=="], + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260329.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zS1thDk7luD82nXVwvMd97F7FgxAE6jGtSmnHeXdaQ+6hJQcQLOVkfUdaehhdodqKDapWA2jEURxQAYjDGvv3g=="], - "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260320.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-uw3sflQVaIIJhfu6xtFFogE8aC0MQ4xt9yRx2CDngp8/PQ0Qp8xp0LrUigQHbwtt7Y8AKR3YCRaX53rbovj4dg=="], + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260329.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-3IJ2qmpjQ1OXpZNUhJRjF1+SbDuqGC14Ug8DjWJlPBp06isi1fcJph90f5qW//FxEsNnJPYRcNwpP0A2RbTASg=="], - "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260320.1", "", { "os": "linux", "cpu": "arm" }, "sha512-tCKwcnikt4uvSfHc14A6FJwEQox1yTsKcssTsAhxycH2bYuUr05AQNUdud3W41tr0MySHrcQG4CstzFxE9C86w=="], + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260329.1", "", { "os": "linux", "cpu": "arm" }, "sha512-WKSSJrH611DFFAg6YCkgbnkdy0a4RRpzvDpNXtPzLTbMYC5oJdq3Dpvncx5nrJvGh4J4yvzXoMxraGPyygqGLw=="], - "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260320.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-fmi8apETnFd6KhLzcGFLUcPRTZO+6vHnFw0Yz08IT68aX2Vft91LnxlJuuaiKVfzE8w9fxQlU1lAZyTVWARGIg=="], + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260329.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gQb6SjB5JlUKDaDuz6mv/m+/OBWVDlcjHINFOykBZZYZtgxBx6nEDjLrT8TiJRjmHEG6hSbv+yisUL9IThWycA=="], - "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260320.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ouNWS+eUHna/mxP0tLQRzBfR5CwldCrG9mi+pTZY1pKnjvvu7b0i6OOFh9em36wIsDCjPGu6dQiv2sfBCCyRwA=="], + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260329.1", "", { "os": "linux", "cpu": "x64" }, "sha512-kg4r+ssxoEWruBynUg9bFMdcMpo5NupzAPqNBlV8uWbmYGZjaPLonFWAi9ZZMiVJY/x5ZQ9GBl6xskwLdd3PJQ=="], - "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260320.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-fG32mrKJOW8U9pq3VdHNXCfxCN3T7Uh5zkf3beH3mFIRtIv65BUONwW6rh63DCtyGZTeBkF6GTJO2nIIfx3dbA=="], + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260329.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-Qi4lddVxl5MG7Tk67gYhCFnoqqLGd4TvaI8RN4qHFjt3GV8s6c+0cQGsJXJnVgMx27qbyDTdsyAa2pvb42rYcQ=="], - "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260320.1", "", { "os": "win32", "cpu": "x64" }, "sha512-iklQlFdDWkV62GbxqVltm7y148vL6TC18BYZRzRaJjf2r+Gd2UGW3FPydNwKViTRfvWWwNx0aU8fnqTaoMBGqw=="], + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260329.1", "", { "os": "win32", "cpu": "x64" }, "sha512-+k5+usuB8HZ6Xc+enLdb95ZJd25bQqsnI1zXxfRCHP+RS9mxs70Mi9ezQz3lKOLZFFXShSH7iW9iulm8KwVzCQ=="], "@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="], @@ -260,7 +260,7 @@ "oxfmt": ["oxfmt@0.35.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.35.0", "@oxfmt/binding-android-arm64": "0.35.0", "@oxfmt/binding-darwin-arm64": "0.35.0", "@oxfmt/binding-darwin-x64": "0.35.0", "@oxfmt/binding-freebsd-x64": "0.35.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.35.0", "@oxfmt/binding-linux-arm-musleabihf": "0.35.0", "@oxfmt/binding-linux-arm64-gnu": "0.35.0", "@oxfmt/binding-linux-arm64-musl": "0.35.0", "@oxfmt/binding-linux-ppc64-gnu": "0.35.0", "@oxfmt/binding-linux-riscv64-gnu": "0.35.0", "@oxfmt/binding-linux-riscv64-musl": "0.35.0", "@oxfmt/binding-linux-s390x-gnu": "0.35.0", "@oxfmt/binding-linux-x64-gnu": "0.35.0", "@oxfmt/binding-linux-x64-musl": "0.35.0", "@oxfmt/binding-openharmony-arm64": "0.35.0", "@oxfmt/binding-win32-arm64-msvc": "0.35.0", "@oxfmt/binding-win32-ia32-msvc": "0.35.0", "@oxfmt/binding-win32-x64-msvc": "0.35.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-QYeXWkP+aLt7utt5SLivNIk09glWx9QE235ODjgcEZ3sd1VMaUBSpLymh6ZRCA76gD2rMP4bXanUz/fx+nLM9Q=="], - "oxlint": ["oxlint@1.56.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.56.0", "@oxlint/binding-android-arm64": "1.56.0", "@oxlint/binding-darwin-arm64": "1.56.0", "@oxlint/binding-darwin-x64": "1.56.0", "@oxlint/binding-freebsd-x64": "1.56.0", "@oxlint/binding-linux-arm-gnueabihf": "1.56.0", "@oxlint/binding-linux-arm-musleabihf": "1.56.0", "@oxlint/binding-linux-arm64-gnu": "1.56.0", "@oxlint/binding-linux-arm64-musl": "1.56.0", "@oxlint/binding-linux-ppc64-gnu": "1.56.0", "@oxlint/binding-linux-riscv64-gnu": "1.56.0", "@oxlint/binding-linux-riscv64-musl": "1.56.0", "@oxlint/binding-linux-s390x-gnu": "1.56.0", "@oxlint/binding-linux-x64-gnu": "1.56.0", "@oxlint/binding-linux-x64-musl": "1.56.0", "@oxlint/binding-openharmony-arm64": "1.56.0", "@oxlint/binding-win32-arm64-msvc": "1.56.0", "@oxlint/binding-win32-ia32-msvc": "1.56.0", "@oxlint/binding-win32-x64-msvc": "1.56.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.15.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-Q+5Mj5PVaH/R6/fhMMFzw4dT+KPB+kQW4kaL8FOIq7tfhlnEVp6+3lcWqFruuTNlUo9srZUW3qH7Id4pskeR6g=="], + "oxlint": ["oxlint@1.57.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.57.0", "@oxlint/binding-android-arm64": "1.57.0", "@oxlint/binding-darwin-arm64": "1.57.0", "@oxlint/binding-darwin-x64": "1.57.0", "@oxlint/binding-freebsd-x64": "1.57.0", "@oxlint/binding-linux-arm-gnueabihf": "1.57.0", "@oxlint/binding-linux-arm-musleabihf": "1.57.0", "@oxlint/binding-linux-arm64-gnu": "1.57.0", "@oxlint/binding-linux-arm64-musl": "1.57.0", "@oxlint/binding-linux-ppc64-gnu": "1.57.0", "@oxlint/binding-linux-riscv64-gnu": "1.57.0", "@oxlint/binding-linux-riscv64-musl": "1.57.0", "@oxlint/binding-linux-s390x-gnu": "1.57.0", "@oxlint/binding-linux-x64-gnu": "1.57.0", "@oxlint/binding-linux-x64-musl": "1.57.0", "@oxlint/binding-openharmony-arm64": "1.57.0", "@oxlint/binding-win32-arm64-msvc": "1.57.0", "@oxlint/binding-win32-ia32-msvc": "1.57.0", "@oxlint/binding-win32-x64-msvc": "1.57.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.15.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-DGFsuBX5MFZX9yiDdtKjTrYPq45CZ8Fft6qCltJITYZxfwYjVdGf/6wycGYTACloauwIPxUnYhBVeZbHvleGhw=="], "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], diff --git a/main.ts b/main.ts index 9e41900..2d63737 100644 --- a/main.ts +++ b/main.ts @@ -2,22 +2,37 @@ import { AgentConfig } from "./src/config"; import { Container } from "./src/container"; import { Renderer } from "./src/renderer"; import { AgentTheme } from "./src/theme"; +import { getCliArgs, showHelp } from "./src/cli"; +import { loadConfig, saveConfig } from "./src/persistent-config"; +import { version } from "./package.json" with { type: "json" }; -/** - * The main entry point for the TUI application. - * - * This function orchestrates the setup of the application's core components: - * 1. It initializes the `AgentConfig` to load configuration. - * 2. It sets up the `Renderer` which manages the lifecycle of the terminal UI. - * 3. It creates the main `Container` component, injecting the renderer and config. - * 4. It calls the `render` method to display the UI. - */ async function main() { - const config = new AgentConfig(); - const theme = new AgentTheme({ - themeName: "default", - mode: "light", - }); + const cliArgs = getCliArgs(); + + if (cliArgs.help) { + showHelp(); + process.exit(0); + } + + if (cliArgs.version) { + console.log(`agent-tui v${version}`); + process.exit(0); + } + + const savedConfig = loadConfig(); + + const mergedTheme = { + ...savedConfig.theme, + ...cliArgs.theme, + }; + + const mergedConfig = { + ...savedConfig.config, + ...cliArgs.config, + }; + + const config = new AgentConfig(mergedConfig); + const theme = new AgentTheme(mergedTheme); const rendererManager = new Renderer(); const renderer = await rendererManager.get(); @@ -28,6 +43,13 @@ async function main() { }); container.render(); + + process.on("exit", () => { + saveConfig({ + config: mergedConfig, + theme: mergedTheme, + }); + }); } main().catch(console.error); diff --git a/package.json b/package.json index d3a8d04..6193548 100644 --- a/package.json +++ b/package.json @@ -12,22 +12,22 @@ "prepare": "simple-git-hooks", "lint": "oxlint .", "lint:fix": "oxlint --fix .", - "format": "oxfmt", + "format": "oxfmt .", "actions:up": "actions-up", "typecheck": "tsgo --noEmit", "dev": "bun --watch main.ts", "start": "bun main.ts" }, "dependencies": { - "@opentui/core": "^0.1.90" + "@opentui/core": "^0.1.92" }, "devDependencies": { "@types/bun": "latest", "@types/node": "^25.5.0", - "@typescript/native-preview": "^7.0.0-dev.20260320.1", + "@typescript/native-preview": "^7.0.0-dev.20260329.1", "actions-up": "^1.12.0", "oxfmt": "^0.35.0", - "oxlint": "^1.56.0", + "oxlint": "^1.57.0", "simple-git-hooks": "^2.13.1" }, "peerDependencies": { diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..9e4c6b6 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,116 @@ +import type { ConfigOpts } from "./config"; +import type { AgentThemeOpts } from "./theme"; + +export interface CliArgs { + config: ConfigOpts; + theme: AgentThemeOpts; + help: boolean; + version: boolean; +} + +const DEFAULT_CLI_ARGS: CliArgs = { + config: {}, + theme: {}, + help: false, + version: false, +}; + +const ALLOWED_THEMES = ["default", "nord", "dracula", "nightowl"] as const; +const THEMES_HELP = ALLOWED_THEMES.join(", "); + +const HELP_TEXT = ` +Usage: agent-tui [options] + +Options: + --theme Theme to use (${THEMES_HELP}) [default: default] + --mode Color mode: light or dark [default: light] + --banner Custom banner text + --no-banner Disable banner display + --help Show this help message + --version Show version number + +Examples: + agent-tui --theme nord --mode dark + agent-tui --banner "Hello World" + agent-tui --no-banner +`; + +function parseArgs(args: string[]): CliArgs { + const result: CliArgs = { ...DEFAULT_CLI_ARGS }; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + switch (arg) { + case "--help": + case "-h": + result.help = true; + break; + + case "--version": + case "-v": + result.version = true; + break; + + case "--theme": { + const themeName = args[++i]; + if (!themeName || themeName?.startsWith("-")) { + console.error("--theme requires a value"); + process.exit(1); + } + if (!ALLOWED_THEMES.includes(themeName as (typeof ALLOWED_THEMES)[number])) { + console.error(`Invalid theme '${themeName}'. Allowed: ${THEMES_HELP}`); + process.exit(1); + } + result.theme.themeName = themeName; + break; + } + + case "--mode": { + const mode = args[++i]; + if (!mode || mode.startsWith("-")) { + console.error("--mode requires a value (light or dark)"); + process.exit(1); + } + if (mode !== "light" && mode !== "dark") { + console.error("--mode must be 'light' or 'dark'"); + process.exit(1); + } + result.theme.mode = mode; + break; + } + + case "--banner": { + const banner = args[++i]; + if (!banner || banner.startsWith("-")) { + console.error("--banner requires a value"); + process.exit(1); + } + result.config.banner = banner; + break; + } + + case "--no-banner": + result.config.banner = ""; + break; + + default: + if (arg?.startsWith("-")) { + console.error(`Unknown option: ${arg}`); + console.error("Use --help for usage information"); + process.exit(1); + } + break; + } + } + + return result; +} + +export function getCliArgs(args: string[] = process.argv.slice(2)): CliArgs { + return parseArgs(args); +} + +export function showHelp(): void { + console.log(HELP_TEXT); +} diff --git a/src/command-palette.ts b/src/command-palette.ts new file mode 100644 index 0000000..cd22e9f --- /dev/null +++ b/src/command-palette.ts @@ -0,0 +1,202 @@ +import { BoxRenderable, InputRenderable, TextRenderable } from "@opentui/core"; +import type { CliRenderer } from "./renderer"; +import type { IAgentTheme, ThemeVariant } from "./theme"; +import figures from "./figures"; + +export interface Command { + id: string; + label: string; + description: string; + action: () => void | Promise; +} + +export interface CommandPaletteOpts { + renderer: CliRenderer; + theme: IAgentTheme; + commands: Command[]; +} + +export class CommandPalette { + private readonly renderer: CliRenderer; + private readonly theme: IAgentTheme; + private readonly commands: Command[]; + private overlay: BoxRenderable | null = null; + private input: InputRenderable | null = null; + private resultsBox: BoxRenderable | null = null; + private selectedIndex = 0; + private filteredCommands: Command[] = []; + private isOpen = false; + + constructor(options: CommandPaletteOpts) { + this.renderer = options.renderer; + this.theme = options.theme; + this.commands = options.commands; + this.filteredCommands = [...this.commands]; + } + + open(): void { + if (this.isOpen) return; + this.isOpen = true; + this.selectedIndex = 0; + this.filteredCommands = [...this.commands]; + this.render(); + } + + close(): void { + if (!this.isOpen) return; + this.isOpen = false; + + if (this.overlay) { + this.overlay.remove("command-palette"); + this.overlay = null; + this.input = null; + this.resultsBox = null; + } + } + + toggle(): void { + if (this.isOpen) { + this.close(); + } else { + this.open(); + } + } + + isVisible(): boolean { + return this.isOpen; + } + + private render(): void { + const _theme: ThemeVariant = this.theme.getTheme(); + + this.overlay = new BoxRenderable(this.renderer, { + id: "command-palette", + flexDirection: "column", + width: "80%", + maxWidth: 60, + backgroundColor: _theme.seeds.neutral, + }); + + const inputContainer = new BoxRenderable(this.renderer, { + id: "command-palette-input", + flexDirection: "row", + height: 3, + backgroundColor: _theme.seeds.primary, + }); + + this.input = new InputRenderable(this.renderer, { + id: "command-input", + placeholder: "Type a command...", + textColor: _theme.seeds.neutral, + marginLeft: 1, + onKeyDown: (key) => { + if (key.name === "escape") { + this.close(); + } else if (key.name === "arrowup") { + this.selectPrevious(); + } else if (key.name === "arrowdown") { + this.selectNext(); + } else if (key.name === "enter") { + this.executeSelected(); + } + }, + }); + + this.input.focus(); + + inputContainer.add(this.input); + + this.resultsBox = new BoxRenderable(this.renderer, { + id: "command-results", + flexDirection: "column", + gap: 0, + height: 10, + }); + + this.renderResults(_theme); + + this.overlay.add(inputContainer); + this.overlay.add(this.resultsBox); + + this.renderer.root.add(this.overlay); + } + + private renderResults(_theme: ThemeVariant): void { + if (!this.resultsBox) return; + + this.resultsBox.remove("command-results"); + + this.resultsBox = new BoxRenderable(this.renderer, { + id: "command-results", + flexDirection: "column", + gap: 0, + height: 10, + }); + + for (let i = 0; i < this.filteredCommands.length; i++) { + const cmd = this.filteredCommands[i]; + if (!cmd) continue; + + const isSelected = i === this.selectedIndex; + const prefix = isSelected ? figures.pointer : " "; + const fg = isSelected ? _theme.seeds.primary : _theme.seeds.neutral; + + const resultItem = new TextRenderable(this.renderer, { + id: `command-result-${cmd.id}`, + content: `${prefix} ${cmd.label}`, + fg, + }); + + const description = new TextRenderable(this.renderer, { + id: `command-desc-${cmd.id}`, + content: ` ${cmd.description}`, + fg: _theme.seeds.info, + }); + + this.resultsBox.add(resultItem); + this.resultsBox.add(description); + } + + this.overlay?.add(this.resultsBox); + } + + private filterCommands(query: string): void { + if (!query.trim()) { + this.filteredCommands = [...this.commands]; + } else { + const lowerQuery = query.toLowerCase(); + this.filteredCommands = this.commands.filter( + (cmd) => + cmd.label.toLowerCase().includes(lowerQuery) || + cmd.description.toLowerCase().includes(lowerQuery), + ); + } + + this.selectedIndex = 0; + const _theme: ThemeVariant = this.theme.getTheme(); + this.renderResults(_theme); + } + + private selectPrevious(): void { + if (this.filteredCommands.length === 0) return; + this.selectedIndex = + (this.selectedIndex - 1 + this.filteredCommands.length) % this.filteredCommands.length; + const _theme: ThemeVariant = this.theme.getTheme(); + this.renderResults(_theme); + } + + private selectNext(): void { + if (this.filteredCommands.length === 0) return; + this.selectedIndex = (this.selectedIndex + 1) % this.filteredCommands.length; + const _theme: ThemeVariant = this.theme.getTheme(); + this.renderResults(_theme); + } + + private executeSelected(): void { + const cmd = this.filteredCommands[this.selectedIndex]; + if (cmd) { + this.close(); + cmd.action(); + } + } +} diff --git a/src/connection-manager.ts b/src/connection-manager.ts new file mode 100644 index 0000000..8beb6f0 --- /dev/null +++ b/src/connection-manager.ts @@ -0,0 +1,148 @@ +import type { ConnectionStatus } from "./types"; +import { store } from "./store"; + +type StatusListener = (status: ConnectionStatus) => void; + +export interface ConnectionManagerOptions { + maxRetries?: number; + retryDelayMs?: number; + healthCheckUrl?: string; +} + +export class ConnectionManager { + private status: ConnectionStatus = "disconnected"; + private listeners: Set = new Set(); + private retryCount = 0; + private maxRetries: number; + private retryDelayMs: number; + private healthCheckUrl?: string; + private healthCheckInterval: ReturnType | null = null; + private isChecking = false; + + constructor(options: ConnectionManagerOptions = {}) { + this.maxRetries = options.maxRetries ?? 5; + this.retryDelayMs = options.retryDelayMs ?? 2000; + this.healthCheckUrl = options.healthCheckUrl; + } + + getStatus(): ConnectionStatus { + return this.status; + } + + subscribe(listener: StatusListener): () => void { + this.listeners.add(listener); + return () => { + this.listeners.delete(listener); + }; + } + + private setStatus(status: ConnectionStatus): void { + this.status = status; + store.setConnectionStatus(status); + + for (const listener of this.listeners) { + listener(status); + } + } + + async connect(): Promise { + if (this.status === "connected" || this.status === "connecting") { + return; + } + + this.setStatus("connecting"); + + try { + if (this.healthCheckUrl) { + await this.performHealthCheck(); + } + + this.retryCount = 0; + this.setStatus("connected"); + this.startHealthCheck(); + } catch (error) { + console.error("Connection failed:", error); + this.setStatus("error"); + await this.scheduleRetry(); + } + } + + async disconnect(): Promise { + this.stopHealthCheck(); + this.setStatus("disconnected"); + } + + private async performHealthCheck(): Promise { + if (!this.healthCheckUrl) return; + + const response = await fetch(this.healthCheckUrl, { + method: "GET", + }); + + if (!response.ok) { + throw new Error(`Health check failed: ${response.status}`); + } + } + + private startHealthCheck(): void { + if (!this.healthCheckUrl || this.healthCheckInterval) return; + + this.healthCheckInterval = setInterval(async () => { + if (this.isChecking) return; + + this.isChecking = true; + try { + await this.performHealthCheck(); + if (this.status !== "connected") { + this.setStatus("connected"); + } + } catch { + this.setStatus("error"); + await this.scheduleRetry(); + } finally { + this.isChecking = false; + } + }, 30000); + } + + private stopHealthCheck(): void { + if (this.healthCheckInterval) { + clearInterval(this.healthCheckInterval); + this.healthCheckInterval = null; + } + } + + private async scheduleRetry(): Promise { + if (this.retryCount >= this.maxRetries) { + console.error(`Max retries (${this.maxRetries}) reached`); + this.setStatus("error"); + return; + } + + this.retryCount++; + const delay = this.retryDelayMs * this.retryCount; + + console.log( + `Retrying connection in ${delay}ms (attempt ${this.retryCount}/${this.maxRetries})`, + ); + + await new Promise((resolve) => setTimeout(resolve, delay)); + + try { + if (this.healthCheckUrl) { + await this.performHealthCheck(); + } + this.retryCount = 0; + this.setStatus("connected"); + } catch { + await this.scheduleRetry(); + } + } + + reset(): void { + this.retryCount = 0; + this.stopHealthCheck(); + } +} + +export const connectionManager = new ConnectionManager(); diff --git a/src/container.ts b/src/container.ts index 8ca6639..6d87794 100644 --- a/src/container.ts +++ b/src/container.ts @@ -1,7 +1,15 @@ -import { BoxRenderable, InputRenderable, TextRenderable } from "@opentui/core"; +import { BoxRenderable, InputRenderable } from "@opentui/core"; import type { IAgentConfig } from "./config"; import type { CliRenderer } from "./renderer"; import type { IAgentTheme, ThemeVariant } from "./theme"; +import { MessageList } from "./message-list"; +import { TypingIndicator } from "./typing-indicator"; +import { CommandPalette, type Command } from "./command-palette"; +import { StatusBar } from "./status-bar"; +import { store } from "./store"; +import { createMessage } from "./types"; +import { LlmClient } from "./llm-client"; +import type { ChatMessage } from "./llm-types"; export interface ContainerOpts { renderer: CliRenderer; @@ -19,12 +27,24 @@ export class Container implements IContainer { private readonly config: IAgentConfig; private readonly theme: IAgentTheme; private readonly container: BoxRenderable; + private messageList: MessageList; + private typingIndicator: TypingIndicator; + private commandPalette: CommandPalette; + private statusBar: StatusBar; + private queryInput: InputRenderable; + private llmClient: LlmClient; constructor(options: ContainerOpts) { this.renderer = options.renderer; this.config = options.config; this.theme = options.theme; + this.llmClient = new LlmClient({ + baseUrl: process.env.LLM_BASE_URL ?? "http://localhost:11434", + apiKey: process.env.LLM_API_KEY ?? "", + model: process.env.LLM_MODEL ?? "llama3", + }); + this.container = new BoxRenderable(this.renderer, { id: "main-container", flexDirection: "column", @@ -32,70 +52,227 @@ export class Container implements IContainer { width: "100%", height: "100%", }); + + this.messageList = new MessageList({ + renderer: this.renderer, + theme: this.theme, + }); + + this.typingIndicator = new TypingIndicator({ + renderer: this.renderer, + theme: this.theme, + }); + + const commands: Command[] = [ + { + id: "clear", + label: "Clear Chat", + description: "Clear all messages", + action: () => { + store.clearMessages(); + this.messageList.clear(); + }, + }, + { + id: "toggle-theme", + label: "Toggle Theme", + description: "Switch between light and dark mode", + action: () => { + store.toggleThemeMode(); + const state = store.getState(); + this.statusBar.updateThemeMode(state.currentThemeName, state.themeMode); + }, + }, + { + id: "quit", + label: "Quit", + description: "Exit the application", + action: () => { + process.exit(0); + }, + }, + ]; + + this.commandPalette = new CommandPalette({ + renderer: this.renderer, + theme: this.theme, + commands, + }); + + this.statusBar = new StatusBar({ + renderer: this.renderer, + theme: this.theme, + }); + + this.queryInput = null as unknown as InputRenderable; + } + + private async sendMessage(content: string): Promise { + if (content.startsWith("/")) { + this.handleCommand(content); + return; + } + + const userMessage = createMessage("user", content); + store.addMessage(userMessage); + this.messageList.addMessage(userMessage); + + this.typingIndicator.show(); + + const messages: ChatMessage[] = store.getState().messages.map((msg) => ({ + role: msg.role === "assistant" ? "assistant" : "user", + content: msg.content, + })); + + const assistantMessage = createMessage("assistant", ""); + store.addMessage(assistantMessage); + this.messageList.addMessage(assistantMessage); + + const currentModel = store.getState().currentModel; + + this.llmClient.chatStream( + { + model: currentModel, + messages, + }, + (chunk) => { + store.updateLastMessage(chunk.delta); + }, + () => { + this.typingIndicator.hide(); + this.statusBar.updateConnectionStatus("connected"); + }, + (error) => { + console.error("LLM Error:", error); + this.typingIndicator.hide(); + this.statusBar.updateConnectionStatus("error"); + }, + ); + + store.subscribe((state) => { + if (state.messages.length > 0) { + const lastMsg = state.messages[state.messages.length - 1]; + if (lastMsg && lastMsg.role === "assistant") { + this.messageList.addMessage(lastMsg); + } + } + }); + } + + private handleCommand(content: string): void { + const parts = content.split(" "); + const command = parts[0]?.toLowerCase() ?? ""; + const args = parts.slice(1).join(" "); + + const state = store.getState(); + + switch (command) { + case "/exit": + case "/quit": + process.exit(0); + break; + + case "/theme": { + const themeName = args.trim() || "default"; + const mode = state.themeMode; + store.setCurrentThemeName(themeName); + this.statusBar.updateThemeMode(themeName, mode); + const response = createMessage("assistant", `Theme set to: ${themeName} (${mode} mode)`); + store.addMessage(response); + this.messageList.addMessage(response); + break; + } + + case "/models": { + const model = args.trim() || "llama3"; + store.setCurrentModel(model); + this.statusBar.updateModel(model); + const response = createMessage("assistant", `Model switched to: ${model}`); + store.addMessage(response); + this.messageList.addMessage(response); + break; + } + + case "/help": { + const helpText = `Available commands: +/theme - Switch theme (default, nord, dracula, nightowl) +/models - Switch model (default: llama3) +/exit - Quit application +Tab - Toggle Plan/Build mode`; + const response = createMessage("assistant", helpText); + store.addMessage(response); + this.messageList.addMessage(response); + break; + } + + default: { + const response = createMessage( + "assistant", + `Unknown command: ${command}. Type /help for available commands.`, + ); + store.addMessage(response); + this.messageList.addMessage(response); + } + } } render() { const _theme: ThemeVariant = this.theme.getTheme(); - const queryContainer = new BoxRenderable(this.renderer, { - id: "query-container", + this.messageList.render(); + + const inputContainer = new BoxRenderable(this.renderer, { + id: "input-container", height: 3, flexDirection: "row", backgroundColor: _theme.overrides?.["text-weak"], }); - const queryPrefix = new BoxRenderable(this.renderer, { - id: "query-prefix", + const inputPrefix = new BoxRenderable(this.renderer, { + id: "input-prefix", backgroundColor: _theme.seeds.primary, }); - const queryInput = new InputRenderable(this.renderer, { + this.queryInput = new InputRenderable(this.renderer, { id: "query-input", - placeholder: "Ask anything...", + placeholder: "Ask anything... (/help for commands)", textColor: _theme.seeds.neutral, marginTop: 1, marginBottom: 1, marginLeft: 2, onKeyDown: (key) => { if (key.name === "escape") { - queryInput.blur(); + this.queryInput.blur(); + } else if (key.ctrl && key.name === "k") { + this.commandPalette.toggle(); + } else if (key.ctrl && key.name === "p") { + process.exit(0); + } else if (key.name === "tab") { + store.toggleAppMode(); + this.statusBar.updateAppMode(store.getState().appMode); + } else if (key.name === "enter") { + const value = (this.queryInput as unknown as { _value: string })._value; + if (value?.trim()) { + this.sendMessage(value.trim()); + } } }, }); - queryInput.focus(); - queryContainer.add(queryPrefix); - queryContainer.add(queryInput); - - const title = new TextRenderable(this.renderer, { - id: "title", - content: this.config.getBanner(), - fg: _theme.seeds.primary, - alignItems: "center", - alignSelf: "center", - }); - - const footerContainer = new BoxRenderable(this.renderer, { - id: "footer-container", - width: "100%", - }); - - const footer = new TextRenderable(this.renderer, { - id: "footer", - content: "Press Ctrl+C to exit", - fg: _theme.seeds.warning, - }); + this.queryInput.focus(); + inputContainer.add(inputPrefix); + inputContainer.add(this.queryInput); - footerContainer.add(footer); + this.statusBar.render(); - this.container.add(title); - this.container.add(queryContainer); - this.container.add(footerContainer); + this.container.add(this.messageList); + this.container.add(inputContainer); + this.container.add(this.statusBar); this.renderer.root.add(this.container); } remove() { - this.container.remove("body"); + this.container.remove("main-container"); } } diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..1fcbed1 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,55 @@ +export class AgentError extends Error { + constructor(message: string) { + super(message); + this.name = "AgentError"; + } +} + +export class RendererError extends AgentError { + constructor( + message: string, + public override readonly cause?: Error, + ) { + super(message); + this.name = "RendererError"; + } +} + +export class ThemeError extends AgentError { + constructor( + message: string, + public override readonly cause?: Error, + ) { + super(message); + this.name = "ThemeError"; + } +} + +export class ConfigError extends AgentError { + constructor( + message: string, + public override readonly cause?: Error, + ) { + super(message); + this.name = "ConfigError"; + } +} + +export class ContainerError extends AgentError { + constructor( + message: string, + public override readonly cause?: Error, + ) { + super(message); + this.name = "ContainerError"; + } +} + +export function wrapError(error: unknown, fallback: T): T { + if (error instanceof Error) { + console.error(error.message); + return fallback; + } + console.error(error); + return fallback; +} diff --git a/src/llm-client.ts b/src/llm-client.ts new file mode 100644 index 0000000..54402da --- /dev/null +++ b/src/llm-client.ts @@ -0,0 +1,186 @@ +import type { + ILlmClient, + LLMClientOptions, + ChatCompletionRequest, + ChatCompletionResponse, + StreamChunk, + Tool, +} from "./llm-types"; + +export class LlmClient implements ILlmClient { + private baseUrl: string; + private apiKey: string; + private model: string; + private tools: Tool[]; + + constructor(options: LLMClientOptions) { + this.baseUrl = options.baseUrl; + this.apiKey = options.apiKey; + this.model = options.model; + this.tools = options.defaultTools ?? []; + } + + setApiKey(apiKey: string): void { + this.apiKey = apiKey; + } + + setBaseUrl(url: string): void { + this.baseUrl = url; + } + + async chat(request: ChatCompletionRequest): Promise { + const response = await fetch(`${this.baseUrl}/chat/completions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.apiKey}`, + }, + body: JSON.stringify({ + ...request, + model: request.model ?? this.model, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`API error: ${response.status} - ${error}`); + } + + return response.json() as Promise; + } + + chatStream( + request: ChatCompletionRequest, + onChunk: (chunk: StreamChunk) => void, + onDone: (finalContent: string) => void, + onError: (error: Error) => void, + ): () => void { + let content = ""; + let finished = false; + let controller: AbortController | null = null; + + const cleanup = () => { + finished = true; + if (controller) { + controller.abort(); + } + }; + + const run = async () => { + controller = new AbortController(); + + try { + const response = await fetch(`${this.baseUrl}/chat/completions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.apiKey}`, + }, + body: JSON.stringify({ + ...request, + model: request.model ?? this.model, + stream: true, + }), + signal: controller.signal, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`API error: ${response.status} - ${error}`); + } + + if (!response.body) { + throw new Error("Response body is null"); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + + while (!finished) { + const { done, value } = await reader.read(); + + if (done) { + break; + } + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || !trimmed.startsWith("data: ")) continue; + + const data = trimmed.slice(6); + if (data === "[DONE]") { + finished = true; + break; + } + + try { + const parsed = JSON.parse(data); + const delta = parsed.choices?.[0]?.delta?.content ?? ""; + const finishReason = parsed.choices?.[0]?.finishReason ?? null; + + if (delta) { + content += delta; + onChunk({ + id: parsed.id ?? "", + delta, + finishReason, + }); + } + + if (finishReason) { + finished = true; + } + } catch { + // Skip malformed JSON + } + } + } + + onDone(content); + } catch (error) { + if ((error as Error).name !== "AbortError") { + onError(error instanceof Error ? error : new Error(String(error))); + } + } + }; + + run(); + + return cleanup; + } + + addTool(tool: Tool): void { + this.tools.push(tool); + } + + removeTool(name: string): void { + this.tools = this.tools.filter((t) => t.name !== name); + } + + getTools(): Tool[] { + return [...this.tools]; + } + + async executeTool(toolCall: { name: string; arguments: string }): Promise { + const tool = this.tools.find((t) => t.name === toolCall.name); + + if (!tool) { + return JSON.stringify({ error: `Tool '${toolCall.name}' not found` }); + } + + try { + const args = JSON.parse(toolCall.arguments); + const result = await tool.execute(args); + return result; + } catch (error) { + return JSON.stringify({ + error: error instanceof Error ? error.message : String(error), + }); + } + } +} diff --git a/src/llm-types.ts b/src/llm-types.ts new file mode 100644 index 0000000..df38215 --- /dev/null +++ b/src/llm-types.ts @@ -0,0 +1,84 @@ +export interface ChatMessage { + role: "system" | "user" | "assistant" | "tool"; + content: string; + toolCalls?: ChatToolCall[]; + toolCallId?: string; +} + +export interface ChatToolCall { + id: string; + type: "function"; + function: { + name: string; + arguments: string; + }; +} + +export interface ChatCompletionRequest { + model: string; + messages: ChatMessage[]; + stream?: boolean; + tools?: ToolDefinition[]; + temperature?: number; + maxTokens?: number; +} + +export interface ChatCompletionResponse { + id: string; + model: string; + choices: { + index: number; + message: ChatMessage; + finishReason: string | null; + }[]; + usage?: { + promptTokens: number; + completionTokens: number; + totalTokens: number; + }; +} + +export interface ToolDefinition { + type: "function"; + function: { + name: string; + description: string; + parameters: Record; + }; +} + +export interface Tool { + name: string; + description: string; + parameters: Record; + execute: (args: Record) => Promise; +} + +export interface StreamChunk { + id: string; + delta: string; + finishReason: string | null; +} + +export type StreamHandler = (chunk: StreamChunk) => void; +export type DoneHandler = (finalContent: string) => void; +export type ErrorHandler = (error: Error) => void; + +export interface LLMClientOptions { + baseUrl: string; + apiKey: string; + model: string; + defaultTools?: Tool[]; +} + +export interface ILlmClient { + chat(request: ChatCompletionRequest): Promise; + chatStream( + request: ChatCompletionRequest, + onChunk: StreamHandler, + onDone: DoneHandler, + onError: ErrorHandler, + ): () => void; + setApiKey(apiKey: string): void; + setBaseUrl(url: string): void; +} diff --git a/src/message-list.ts b/src/message-list.ts new file mode 100644 index 0000000..2f35fda --- /dev/null +++ b/src/message-list.ts @@ -0,0 +1,109 @@ +import { BoxRenderable, TextRenderable } from "@opentui/core"; +import type { CliRenderer } from "./renderer"; +import type { IAgentTheme, ThemeVariant } from "./theme"; +import type { Message } from "./types"; +import figures from "./figures"; + +export interface MessageListOpts { + renderer: CliRenderer; + theme: IAgentTheme; +} + +export interface IMessageList { + render(): void; + update(): void; + addMessage(message: Message): void; +} + +export class MessageList implements IMessageList { + private readonly renderer: CliRenderer; + private readonly theme: IAgentTheme; + private container: BoxRenderable; + private messages: Message[] = []; + + constructor(options: MessageListOpts) { + this.renderer = options.renderer; + this.theme = options.theme; + + this.container = new BoxRenderable(this.renderer, { + id: "message-list", + flexDirection: "column", + gap: 1, + width: "100%", + }); + } + + render(): void { + const _theme: ThemeVariant = this.theme.getTheme(); + + this.container = new BoxRenderable(this.renderer, { + id: "message-list", + flexDirection: "column", + gap: 1, + width: "100%", + backgroundColor: _theme.overrides?.["bg"], + }); + + this.renderer.root.add(this.container); + } + + update(): void { + this.container.remove("message-list"); + + const _theme: ThemeVariant = this.theme.getTheme(); + + this.container = new BoxRenderable(this.renderer, { + id: "message-list", + flexDirection: "column", + gap: 1, + width: "100%", + backgroundColor: _theme.overrides?.["bg"], + }); + + for (const message of this.messages) { + this.addMessageToContainer(message, _theme); + } + + this.renderer.root.add(this.container); + } + + addMessage(message: Message): void { + this.messages.push(message); + const _theme: ThemeVariant = this.theme.getTheme(); + this.addMessageToContainer(message, _theme); + } + + private addMessageToContainer(message: Message, _theme: ThemeVariant): void { + const prefix = message.role === "user" ? figures.pointer : figures.triangleRight; + const prefixColor = message.role === "user" ? _theme.seeds.primary : _theme.seeds.success; + const roleLabel = message.role === "user" ? "You" : "Assistant"; + + const messageBox = new BoxRenderable(this.renderer, { + id: `message-${message.id}`, + flexDirection: "column", + width: "100%", + }); + + const header = new TextRenderable(this.renderer, { + id: `message-header-${message.id}`, + content: `${prefix} ${roleLabel}`, + fg: prefixColor, + }); + + const content = new TextRenderable(this.renderer, { + id: `message-content-${message.id}`, + content: message.content, + fg: _theme.seeds.neutral, + marginLeft: 2, + }); + + messageBox.add(header); + messageBox.add(content); + this.container.add(messageBox); + } + + clear(): void { + this.messages = []; + this.update(); + } +} diff --git a/src/persistent-config.ts b/src/persistent-config.ts new file mode 100644 index 0000000..d2cc5f1 --- /dev/null +++ b/src/persistent-config.ts @@ -0,0 +1,69 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { homedir } from "node:os"; +import type { ConfigOpts } from "./config"; +import type { AgentThemeOpts } from "./theme"; + +export interface PersistentConfig { + config: ConfigOpts; + theme: AgentThemeOpts; +} + +const DEFAULT_CONFIG: PersistentConfig = { + config: {}, + theme: {}, +}; + +const CONFIG_DIR = join(homedir(), ".agent-tui"); +const CONFIG_FILE = join(CONFIG_DIR, "config.json"); + +function ensureConfigDir(): void { + if (!existsSync(CONFIG_DIR)) { + mkdirSync(CONFIG_DIR, { recursive: true }); + } +} + +export function loadConfig(): PersistentConfig { + try { + if (!existsSync(CONFIG_FILE)) { + return DEFAULT_CONFIG; + } + + const data = readFileSync(CONFIG_FILE, "utf-8"); + const parsed = JSON.parse(data); + + if (typeof parsed !== "object" || parsed === null) { + console.error("Invalid config file, using defaults"); + return DEFAULT_CONFIG; + } + + return { + config: parsed.config ?? DEFAULT_CONFIG.config, + theme: parsed.theme ?? DEFAULT_CONFIG.theme, + }; + } catch (error) { + console.error("Failed to load config:", error); + return DEFAULT_CONFIG; + } +} + +export function saveConfig(config: PersistentConfig): void { + try { + ensureConfigDir(); + writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); + } catch (error) { + console.error("Failed to save config:", error); + } +} + +export function resetConfig(): void { + try { + saveConfig(DEFAULT_CONFIG); + } catch (error) { + console.error("Failed to reset config:", error); + } +} + +export function getConfigPath(): string { + return CONFIG_FILE; +} diff --git a/src/renderer.ts b/src/renderer.ts index e265229..f72595d 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -1,4 +1,8 @@ import { createCliRenderer } from "@opentui/core"; +import { RendererError } from "./errors"; + +const MAX_RETRIES = 3; +const RETRY_DELAY_MS = 500; export type CliRenderer = Awaited>; export type CliRendererOptions = Parameters[0]; @@ -43,13 +47,37 @@ export class Renderer implements IRenderer { /** * Lazily creates and returns a single instance of the CLI renderer. + * Includes retry logic for failed initializations. * @returns A promise that resolves to the CliRenderer instance. */ async get(): Promise { - if (!this.instance) { - this.instance = await createCliRenderer(this.options); + if (this.instance) { + return this.instance; } - return this.instance; + + let lastError: Error | undefined; + + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + this.instance = await createCliRenderer(this.options); + return this.instance; + } catch (error) { + lastError = error instanceof Error ? error : new RendererError(String(error)); + console.error( + `Renderer initialization attempt ${attempt}/${MAX_RETRIES} failed:`, + lastError.message, + ); + + if (attempt < MAX_RETRIES) { + await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS * attempt)); + } + } + } + + throw new RendererError( + `Failed to initialize renderer after ${MAX_RETRIES} attempts`, + lastError, + ); } /** diff --git a/src/status-bar.ts b/src/status-bar.ts new file mode 100644 index 0000000..9df881a --- /dev/null +++ b/src/status-bar.ts @@ -0,0 +1,187 @@ +import { BoxRenderable, TextRenderable } from "@opentui/core"; +import { cwd } from "node:process"; +import type { CliRenderer } from "./renderer"; +import type { IAgentTheme, ThemeVariant } from "./theme"; +import type { ConnectionStatus, AppMode } from "./types"; +import figures from "./figures"; +import { version } from "../package.json" with { type: "json" }; + +export interface StatusBarOpts { + renderer: CliRenderer; + theme: IAgentTheme; +} + +export class StatusBar { + private readonly renderer: CliRenderer; + private readonly theme: IAgentTheme; + private container: BoxRenderable | null = null; + private leftBox: BoxRenderable | null = null; + private rightBox: BoxRenderable | null = null; + + constructor(options: StatusBarOpts) { + this.renderer = options.renderer; + this.theme = options.theme; + } + + render(): void { + const _theme: ThemeVariant = this.theme.getTheme(); + const currentDir = cwd().split(/[/\\]/).pop() ?? "agent-tui"; + + this.container = new BoxRenderable(this.renderer, { + id: "status-bar", + flexDirection: "row", + width: "100%", + height: 1, + }); + + this.leftBox = new BoxRenderable(this.renderer, { + id: "status-left", + flexDirection: "row", + gap: 1, + }); + + this.rightBox = new BoxRenderable(this.renderer, { + id: "status-right", + flexDirection: "row", + gap: 1, + }); + + const versionText = new TextRenderable(this.renderer, { + id: "status-version", + content: `v${version}`, + fg: _theme.seeds.info, + }); + + const dirText = new TextRenderable(this.renderer, { + id: "status-dir", + content: currentDir, + fg: _theme.seeds.neutral, + }); + + const connectionText = new TextRenderable(this.renderer, { + id: "status-connection", + content: `${figures.circle}Offline`, + fg: _theme.seeds.warning, + }); + + const modeText = new TextRenderable(this.renderer, { + id: "status-mode", + content: "[Plan]", + fg: _theme.seeds.primary, + }); + + const themeText = new TextRenderable(this.renderer, { + id: "status-theme", + content: "theme:default", + fg: _theme.seeds.info, + }); + + const modelText = new TextRenderable(this.renderer, { + id: "status-model", + content: "model:llama3", + fg: _theme.seeds.success, + }); + + const helpText = new TextRenderable(this.renderer, { + id: "status-help", + content: "Tab:Mode Ctrl+P:Cmd", + fg: _theme.seeds.neutral, + }); + + this.leftBox.add(versionText); + this.leftBox.add(dirText); + this.leftBox.add(connectionText); + + this.rightBox.add(modeText); + this.rightBox.add(themeText); + this.rightBox.add(modelText); + this.rightBox.add(helpText); + + this.container.add(this.leftBox); + this.container.add(this.rightBox); + this.renderer.root.add(this.container); + } + + updateConnectionStatus(status: ConnectionStatus): void { + if (!this.leftBox) return; + + const _theme: ThemeVariant = this.theme.getTheme(); + let icon: string; + let text: string; + let fg: string; + + switch (status) { + case "connected": + icon = figures.tick; + text = "Online"; + fg = _theme.seeds.success; + break; + case "connecting": + icon = figures.ellipsis; + text = "Connecting"; + fg = _theme.seeds.warning; + break; + case "error": + icon = figures.cross; + text = "Error"; + fg = _theme.seeds.error; + break; + case "disconnected": + default: + icon = figures.circle; + text = "Offline"; + fg = _theme.seeds.warning; + break; + } + + this.leftBox.remove("status-connection"); + const connectionText = new TextRenderable(this.renderer, { + id: "status-connection", + content: `${icon}${text}`, + fg, + }); + this.leftBox.add(connectionText); + } + + updateThemeMode(themeName: string, _mode: "light" | "dark"): void { + if (!this.rightBox) return; + + const _theme: ThemeVariant = this.theme.getTheme(); + + this.rightBox.remove("status-theme"); + const themeText = new TextRenderable(this.renderer, { + id: "status-theme", + content: `theme:${themeName}`, + fg: _theme.seeds.info, + }); + this.rightBox.add(themeText); + } + + updateModel(model: string): void { + if (!this.rightBox) return; + + const _theme: ThemeVariant = this.theme.getTheme(); + + this.rightBox.remove("status-model"); + const modelText = new TextRenderable(this.renderer, { + id: "status-model", + content: `model:${model}`, + fg: _theme.seeds.success, + }); + this.rightBox.add(modelText); + } + + updateAppMode(mode: AppMode): void { + if (!this.rightBox) return; + + const _theme: ThemeVariant = this.theme.getTheme(); + + this.rightBox.remove("status-mode"); + const modeText = new TextRenderable(this.renderer, { + id: "status-mode", + content: `[${mode.toUpperCase()}]`, + fg: _theme.seeds.primary, + }); + this.rightBox.add(modeText); + } +} diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..11e4ecd --- /dev/null +++ b/src/store.ts @@ -0,0 +1,101 @@ +import type { AppState, Message, ConnectionStatus, AppMode } from "./types"; + +type Listener = (state: AppState) => void; + +class StateStore { + private state: AppState = { + messages: [], + isTyping: false, + connectionStatus: "disconnected", + themeMode: "light", + appMode: "plan", + currentModel: "llama3", + currentThemeName: "default", + }; + + private listeners: Set = new Set(); + + getState(): AppState { + return this.state; + } + + subscribe(listener: Listener): () => void { + this.listeners.add(listener); + return () => { + this.listeners.delete(listener); + }; + } + + private notify(): void { + for (const listener of this.listeners) { + listener(this.state); + } + } + + addMessage(message: Message): void { + this.state = { + ...this.state, + messages: [...this.state.messages, message], + }; + this.notify(); + } + + updateLastMessage(content: string): void { + if (this.state.messages.length === 0) return; + + const messages = [...this.state.messages]; + const lastMessage = messages[messages.length - 1]; + if (!lastMessage) return; + + messages[messages.length - 1] = { + ...lastMessage, + content: lastMessage.content + content, + }; + + this.state = { ...this.state, messages }; + this.notify(); + } + + clearMessages(): void { + this.state = { ...this.state, messages: [] }; + this.notify(); + } + + setTyping(isTyping: boolean): void { + this.state = { ...this.state, isTyping }; + this.notify(); + } + + setConnectionStatus(status: ConnectionStatus): void { + this.state = { ...this.state, connectionStatus: status }; + this.notify(); + } + + setThemeMode(mode: "light" | "dark"): void { + this.state = { ...this.state, themeMode: mode }; + this.notify(); + } + + toggleThemeMode(): void { + const newMode = this.state.themeMode === "light" ? "dark" : "light"; + this.setThemeMode(newMode); + } + + toggleAppMode(): void { + const newMode: AppMode = this.state.appMode === "plan" ? "build" : "plan"; + this.state = { ...this.state, appMode: newMode }; + this.notify(); + } + + setCurrentModel(model: string): void { + this.state = { ...this.state, currentModel: model }; + this.notify(); + } + + setCurrentThemeName(themeName: string): void { + this.state = { ...this.state, currentThemeName: themeName }; + this.notify(); + } +} + +export const store = new StateStore(); diff --git a/src/theme.ts b/src/theme.ts index e5e8139..7ce3ada 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -1,5 +1,6 @@ import { existsSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; +import { ThemeError } from "./errors"; const ALLOWED_THEMES = ["default", "nord", "dracula", "nightowl"] as const; @@ -111,7 +112,11 @@ export class AgentTheme implements IAgentTheme { const parsed = JSON.parse(themeFile); return this.validateTheme(parsed); } catch (error) { - console.error(`Failed to parse theme '${themeName}':`, error); + const themeError: ThemeError = new ThemeError( + `Failed to load theme '${themeName}'`, + error instanceof Error ? error : undefined, + ); + console.error(themeError.message); return DEFAULT_THEME; } } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..1d813f4 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,54 @@ +export type MessageRole = "user" | "assistant" | "system"; + +export interface Message { + id: string; + role: MessageRole; + content: string; + timestamp: number; +} + +export interface ToolCall { + id: string; + name: string; + arguments: string; + result?: string; +} + +export interface ResponseStream { + messageId: string; + delta: string; + done: boolean; +} + +export type ConnectionStatus = "connected" | "disconnected" | "connecting" | "error"; + +export type AppMode = "plan" | "build"; + +export interface AppState { + messages: Message[]; + isTyping: boolean; + connectionStatus: ConnectionStatus; + themeMode: "light" | "dark"; + appMode: AppMode; + currentModel: string; + currentThemeName: string; +} + +let messageIdCounter = 0; + +export function createMessage(role: MessageRole, content: string): Message { + return { + id: `msg_${++messageIdCounter}`, + role, + content, + timestamp: Date.now(), + }; +} + +export function createToolCall(name: string, args: string): ToolCall { + return { + id: `tool_${++messageIdCounter}`, + name, + arguments: args, + }; +} diff --git a/src/typing-indicator.ts b/src/typing-indicator.ts new file mode 100644 index 0000000..27b6f21 --- /dev/null +++ b/src/typing-indicator.ts @@ -0,0 +1,87 @@ +import { BoxRenderable, TextRenderable } from "@opentui/core"; +import type { CliRenderer } from "./renderer"; +import type { IAgentTheme, ThemeVariant } from "./theme"; + +export interface TypingIndicatorOpts { + renderer: CliRenderer; + theme: IAgentTheme; +} + +export class TypingIndicator { + private readonly renderer: CliRenderer; + private readonly theme: IAgentTheme; + private container: BoxRenderable | null = null; + private dotsComponent: TextRenderable | null = null; + private dots = 0; + private intervalId: ReturnType | null = null; + + constructor(options: TypingIndicatorOpts) { + this.renderer = options.renderer; + this.theme = options.theme; + } + + show(): void { + if (this.container) return; + + const _theme: ThemeVariant = this.theme.getTheme(); + + this.container = new BoxRenderable(this.renderer, { + id: "typing-indicator", + flexDirection: "row", + gap: 1, + }); + + const label = new TextRenderable(this.renderer, { + id: "typing-label", + content: "Assistant is thinking", + fg: _theme.seeds.info, + }); + + this.dotsComponent = new TextRenderable(this.renderer, { + id: "typing-dots", + content: "...", + fg: _theme.seeds.info, + }); + + this.container.add(label); + this.container.add(this.dotsComponent); + + this.intervalId = setInterval(() => { + this.dots = (this.dots + 1) % 4; + const dotsText = ".".repeat(this.dots).padEnd(3); + this.updateDots(dotsText); + }, 300); + + this.renderer.root.add(this.container); + } + + private updateDots(text: string): void { + if (this.dotsComponent) { + this.dotsComponent.remove("typing-dots"); + const _theme: ThemeVariant = this.theme.getTheme(); + this.dotsComponent = new TextRenderable(this.renderer, { + id: "typing-dots", + content: text, + fg: _theme.seeds.info, + }); + this.container?.add(this.dotsComponent); + } + } + + hide(): void { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + + if (this.container) { + this.container.remove("typing-indicator"); + this.container = null; + this.dotsComponent = null; + } + } + + isVisible(): boolean { + return this.container !== null; + } +} From 2400d93821419cbc59007d26e20aab9470687267 Mon Sep 17 00:00:00 2001 From: Joydip Roy Date: Mon, 30 Mar 2026 09:43:17 +0530 Subject: [PATCH 6/6] docs: update README and architecture docs with latest changes --- .opencode/skills/git-release/SKILL.md | 20 ------- README.md | 86 ++++++++++++++++++++------- bun.lock | 6 +- docs/ARCHITECTURE.md | 73 ++++++++++++++++++----- 4 files changed, 127 insertions(+), 58 deletions(-) delete mode 100644 .opencode/skills/git-release/SKILL.md diff --git a/.opencode/skills/git-release/SKILL.md b/.opencode/skills/git-release/SKILL.md deleted file mode 100644 index 959f8de..0000000 --- a/.opencode/skills/git-release/SKILL.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: git-release -description: Create consistent releases and changelogs -license: MIT -compatibility: opencode -metadata: - audience: maintainers - workflow: github ---- - -## What I do - -- Draft release notes from merged PRs -- Propose a version bump -- Provide a copy-pasteable `gh release create` command - -## When to use me - -Use this when you are preparing a tagged release. -Ask clarifying questions if the target versioning scheme is unclear. diff --git a/README.md b/README.md index 45e1124..158ce28 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,34 @@ -# try-opentui +# Agent TUI -## Project Overview +A Bun-based TypeScript TUI (Terminal User Interface) application using `@opentui/core` for rendering. -This is a Bun-based TypeScript TUI (Terminal User Interface) application using `@opentui/core` for rendering. +## Features -## Development & CLI Commands Guide +- **Chat Interface**: Interactive chat with AI assistant +- **Command Palette**: Press `Ctrl+K` to access commands +- **Theme Switching**: Multiple themes (default, nord, dracula, nightowl) +- **Model Switching**: Support for multiple LLM models +- **Keyboard Shortcuts**: Efficient keyboard-driven navigation -### Installation & Running +## Commands + +| Command | Description | +| ------- | ----------- | +| `/theme ` | Switch theme (default, nord, dracula, nightowl) | +| `/models ` | Switch LLM model | +| `/exit` | Exit application | +| `/help` | Show available commands | + +## Keyboard Shortcuts + +| Shortcut | Action | +| -------- | ------ | +| `Tab` | Toggle Plan/Build mode | +| `Ctrl+K` | Open command palette | +| `Ctrl+P` | Quit application | +| `Enter` | Send message | + +## Installation & Running ```bash bun install # Install dependencies @@ -14,7 +36,7 @@ bun dev # Run in development mode with file watching bun start # Run the application ``` -### Linting & Formatting +## Linting & Formatting ```bash bun lint # Run oxlint to check code quality @@ -22,31 +44,53 @@ bun lint:fix # Run oxlint with auto-fix bun format # Run oxfmt to format code ``` -### Type Checking & Pre-commit +## Type Checking & Pre-commit ```bash bun typecheck # Run TypeScript type checking (tsgo) bun run actions:up # Run actions-up (updates?) ``` -**Note:** There is no test framework configured in this project yet. +## Testing + +```bash +bun test # Run tests +``` + +## Environment Variables ---- +| Variable | Default | Description | +| -------- | ------- | ----------- | +| `LLM_BASE_URL` | http://localhost:11434 | LLM API endpoint | +| `LLM_API_KEY` | - | API key for authentication | +| `LLM_MODEL` | llama3 | Default model to use | ## File Organization ```bash -├── main.ts # Entry point +├── main.ts # Entry point ├── src/ -│ ├── config.ts # Configuration management -│ ├── container.ts # Main UI container component -│ ├── renderer.ts # Renderer lifecycle management -│ ├── theme.ts # Theme loading and management -│ ├── utils.ts # Utility functions -│ └── figures.ts # Figure/shape utilities -├── docs/ # Documentation -├── themes/ # Theme JSON files -├── .oxlintrc.json # Linting rules -├── .oxfmtrc.json # Formatting rules -└── tsconfig.json # TypeScript configuration +│ ├── cli.ts # CLI argument parsing +│ ├── config.ts # Configuration management +│ ├── container.ts # Main UI container component +│ ├── renderer.ts # Renderer lifecycle management +│ ├── theme.ts # Theme loading and management +│ ├── utils.ts # Utility functions +│ ├── figures.ts # Figure/shape utilities +│ ├── errors.ts # Error types +│ ├── types.ts # TypeScript type definitions +│ ├── store.ts # State management +│ ├── message-list.ts # Chat message display +│ ├── typing-indicator.ts # Thinking indicator +│ ├── command-palette.ts # Command palette UI +│ ├── status-bar.ts # Bottom status bar +│ ├── llm-types.ts # LLM client types +│ ├── llm-client.ts # LLM HTTP client +│ └── connection-manager.ts # Connection management +├── test/ # Test files +├── docs/ # Documentation +├── themes/ # Theme JSON files +├── .oxlintrc.json # Linting rules +├── .oxfmtrc.json # Formatting rules +└── tsconfig.json # TypeScript configuration ``` diff --git a/bun.lock b/bun.lock index 0821149..d02078d 100644 --- a/bun.lock +++ b/bun.lock @@ -5,15 +5,15 @@ "": { "name": "try-opentui", "dependencies": { - "@opentui/core": "^0.1.90", + "@opentui/core": "^0.1.92", }, "devDependencies": { "@types/bun": "latest", "@types/node": "^25.5.0", - "@typescript/native-preview": "^7.0.0-dev.20260320.1", + "@typescript/native-preview": "^7.0.0-dev.20260329.1", "actions-up": "^1.12.0", "oxfmt": "^0.35.0", - "oxlint": "^1.56.0", + "oxlint": "^1.57.0", "simple-git-hooks": "^2.13.1", }, "peerDependencies": { diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 5bd7ad5..04f5b2e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -4,33 +4,78 @@ The application follows a layered architecture with clear separation of concerns: -```bash +``` main.ts (Entry Point) ├── AgentConfig (Configuration) ├── AgentTheme (Theme Management) ├── Renderer (Lifecycle/Singleton Manager) │ └── createCliRenderer (@opentui/core) +├── LlmClient (LLM API Client) +│ ├── chat() - Non-streaming requests +│ └── chatStream() - Streaming responses +├── ConnectionManager (Connection Management) └── Container (UI Composition) -├── BoxRenderable (@opentui/core) -├── InputRenderable (@opentui/core) -└── TextRenderable (@opentui/core) + ├── MessageList (Chat history) + ├── TypingIndicator (Thinking display) + ├── CommandPalette (Command menu) + └── StatusBar (Bottom status) ``` ## Component Breakdown -| Component | File | Responsibility | Pattern | -| ----------- | ------------ | --------------------------------- | ----------------- | -| main.ts | Entry point | Orchestrates initialization | Procedural | -| AgentConfig | config.ts | Banner/configuration management | Simple Config | -| AgentTheme | theme.ts | Theme loading & variant selection | Strategy | -| Renderer | renderer.ts | CLI renderer lifecycle management | Singleton Manager | -| Container | container.ts | UI component composition | Composite | -| figures.ts | figures.ts | Unicode figure symbols | Utility | -| utils.ts | utils.ts | Terminal Unicode detection | Utility | +| Component | File | Responsibility | Pattern | +| --------- | ---- | -------------- | ------- | +| main.ts | Entry point | Orchestrates initialization | Procedural | +| AgentConfig | config.ts | Banner/configuration management | Simple Config | +| AgentTheme | theme.ts | Theme loading & variant selection | Strategy | +| Renderer | renderer.ts | CLI renderer lifecycle management | Singleton Manager | +| Container | container.ts | UI component composition | Composite | +| LlmClient | llm-client.ts | LLM API communication | Adapter | +| ConnectionManager | connection-manager.ts | Connection health & retry | State Machine | +| StateStore | store.ts | Application state management | Observer | +| MessageList | message-list.ts | Chat message display | Composite | +| CommandPalette | command-palette.ts | Command selection UI | Composite | +| StatusBar | status-bar.ts | Status display | Composite | +| figures.ts | figures.ts | Unicode figure symbols | Utility | +| utils.ts | utils.ts | Terminal Unicode detection | Utility | +| errors.ts | errors.ts | Error type definitions | Error Handling | + +## State Management + +The application uses a simple pub/sub state store: + +```typescript +interface AppState { + messages: Message[]; + isTyping: boolean; + connectionStatus: ConnectionStatus; + themeMode: "light" | "dark"; + appMode: "plan" | "build"; + currentModel: string; + currentThemeName: string; +} +``` + +## LLM Client + +Supports streaming and non-streaming chat completions: + +```typescript +// Non-streaming +const response = await client.chat(request); + +// Streaming +const cleanup = client.chatStream( + request, + (chunk) => { /* handle delta */ }, + (final) => { /* handle done */ }, + (error) => { /* handle error */ } +); +``` ## Dependency Flow -```bash +``` @opentui/core ├── createCliRenderer() → CliRenderer ├── BoxRenderable (layout container)