mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 13:03:42 +00:00
Release v0.8.0
This commit is contained in:
parent
cc88095140
commit
85adcf22bf
48 changed files with 1530 additions and 608 deletions
428
package-lock.json
generated
428
package-lock.json
generated
|
|
@ -5413,7 +5413,7 @@
|
|||
},
|
||||
"packages/agent": {
|
||||
"name": "@mariozechner/pi-agent",
|
||||
"version": "0.7.29",
|
||||
"version": "0.8.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-ai": "^0.7.29",
|
||||
|
|
@ -5428,6 +5428,42 @@
|
|||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"packages/agent/node_modules/@mariozechner/pi-ai": {
|
||||
"version": "0.7.29",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.7.29.tgz",
|
||||
"integrity": "sha512-CtYAyQikG4S2j22+8OBclCBASTMzmyfca32K9SMLIV2W88/1JdJL3B9TfZOdPrp/6WMFRe+TdprXh0UK/I/Ikw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.61.0",
|
||||
"@google/genai": "^1.30.0",
|
||||
"@sinclair/typebox": "^0.34.41",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"chalk": "^5.6.2",
|
||||
"openai": "5.21.0",
|
||||
"partial-json": "^0.1.7",
|
||||
"zod-to-json-schema": "^3.24.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"packages/agent/node_modules/@mariozechner/pi-tui": {
|
||||
"version": "0.7.29",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.7.29.tgz",
|
||||
"integrity": "sha512-vqrjc2NynNj+4y5WVabPQZSBkWw2NIYIz4VSROtqkGQD5ilaalZuHd+oIsAByClFlJv0joaa+Qtwbcvfl1nTiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"chalk": "^5.5.0",
|
||||
"marked": "^15.0.12",
|
||||
"mime-types": "^3.0.1",
|
||||
"string-width": "^8.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"packages/agent/node_modules/@types/node": {
|
||||
"version": "24.10.1",
|
||||
"dev": true,
|
||||
|
|
@ -5436,6 +5472,73 @@
|
|||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"packages/agent/node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"packages/agent/node_modules/chalk": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"packages/agent/node_modules/marked": {
|
||||
"version": "15.0.12",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
||||
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"packages/agent/node_modules/string-width": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
|
||||
"integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-east-asian-width": "^1.3.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/agent/node_modules/strip-ansi": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"packages/agent/node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"dev": true,
|
||||
|
|
@ -5443,7 +5546,7 @@
|
|||
},
|
||||
"packages/ai": {
|
||||
"name": "@mariozechner/pi-ai",
|
||||
"version": "0.7.29",
|
||||
"version": "0.8.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.61.0",
|
||||
|
|
@ -5490,7 +5593,7 @@
|
|||
},
|
||||
"packages/coding-agent": {
|
||||
"name": "@mariozechner/pi-coding-agent",
|
||||
"version": "0.7.29",
|
||||
"version": "0.8.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-agent": "^0.7.29",
|
||||
|
|
@ -5512,6 +5615,55 @@
|
|||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"packages/coding-agent/node_modules/@mariozechner/pi-agent": {
|
||||
"version": "0.7.29",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-agent/-/pi-agent-0.7.29.tgz",
|
||||
"integrity": "sha512-zmgg0Ob6xzrQP8AcxwziH1YR+RmIb/kNWTltXTr9kTpXZPhXh/kFIc56ZhWxkMl5BsY9AOvSRgWv+EPCoJFglQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-ai": "^0.7.29",
|
||||
"@mariozechner/pi-tui": "^0.7.29"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"packages/coding-agent/node_modules/@mariozechner/pi-ai": {
|
||||
"version": "0.7.29",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.7.29.tgz",
|
||||
"integrity": "sha512-CtYAyQikG4S2j22+8OBclCBASTMzmyfca32K9SMLIV2W88/1JdJL3B9TfZOdPrp/6WMFRe+TdprXh0UK/I/Ikw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.61.0",
|
||||
"@google/genai": "^1.30.0",
|
||||
"@sinclair/typebox": "^0.34.41",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"chalk": "^5.6.2",
|
||||
"openai": "5.21.0",
|
||||
"partial-json": "^0.1.7",
|
||||
"zod-to-json-schema": "^3.24.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"packages/coding-agent/node_modules/@mariozechner/pi-tui": {
|
||||
"version": "0.7.29",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.7.29.tgz",
|
||||
"integrity": "sha512-vqrjc2NynNj+4y5WVabPQZSBkWw2NIYIz4VSROtqkGQD5ilaalZuHd+oIsAByClFlJv0joaa+Qtwbcvfl1nTiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"chalk": "^5.5.0",
|
||||
"marked": "^15.0.12",
|
||||
"mime-types": "^3.0.1",
|
||||
"string-width": "^8.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"packages/coding-agent/node_modules/@types/node": {
|
||||
"version": "24.10.1",
|
||||
"dev": true,
|
||||
|
|
@ -5520,6 +5672,18 @@
|
|||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"packages/coding-agent/node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"packages/coding-agent/node_modules/chalk": {
|
||||
"version": "5.6.2",
|
||||
"license": "MIT",
|
||||
|
|
@ -5530,6 +5694,49 @@
|
|||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"packages/coding-agent/node_modules/marked": {
|
||||
"version": "15.0.12",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
||||
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"packages/coding-agent/node_modules/string-width": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
|
||||
"integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-east-asian-width": "^1.3.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/coding-agent/node_modules/strip-ansi": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"packages/coding-agent/node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"dev": true,
|
||||
|
|
@ -5537,7 +5744,7 @@
|
|||
},
|
||||
"packages/pods": {
|
||||
"name": "@mariozechner/pi",
|
||||
"version": "0.7.29",
|
||||
"version": "0.8.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-agent": "^0.7.29",
|
||||
|
|
@ -5551,6 +5758,67 @@
|
|||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"packages/pods/node_modules/@mariozechner/pi-agent": {
|
||||
"version": "0.7.29",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-agent/-/pi-agent-0.7.29.tgz",
|
||||
"integrity": "sha512-zmgg0Ob6xzrQP8AcxwziH1YR+RmIb/kNWTltXTr9kTpXZPhXh/kFIc56ZhWxkMl5BsY9AOvSRgWv+EPCoJFglQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-ai": "^0.7.29",
|
||||
"@mariozechner/pi-tui": "^0.7.29"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"packages/pods/node_modules/@mariozechner/pi-ai": {
|
||||
"version": "0.7.29",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.7.29.tgz",
|
||||
"integrity": "sha512-CtYAyQikG4S2j22+8OBclCBASTMzmyfca32K9SMLIV2W88/1JdJL3B9TfZOdPrp/6WMFRe+TdprXh0UK/I/Ikw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.61.0",
|
||||
"@google/genai": "^1.30.0",
|
||||
"@sinclair/typebox": "^0.34.41",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"chalk": "^5.6.2",
|
||||
"openai": "5.21.0",
|
||||
"partial-json": "^0.1.7",
|
||||
"zod-to-json-schema": "^3.24.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"packages/pods/node_modules/@mariozechner/pi-tui": {
|
||||
"version": "0.7.29",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.7.29.tgz",
|
||||
"integrity": "sha512-vqrjc2NynNj+4y5WVabPQZSBkWw2NIYIz4VSROtqkGQD5ilaalZuHd+oIsAByClFlJv0joaa+Qtwbcvfl1nTiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"chalk": "^5.5.0",
|
||||
"marked": "^15.0.12",
|
||||
"mime-types": "^3.0.1",
|
||||
"string-width": "^8.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"packages/pods/node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"packages/pods/node_modules/chalk": {
|
||||
"version": "5.6.2",
|
||||
"license": "MIT",
|
||||
|
|
@ -5561,9 +5829,52 @@
|
|||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"packages/pods/node_modules/marked": {
|
||||
"version": "15.0.12",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
||||
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"packages/pods/node_modules/string-width": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
|
||||
"integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-east-asian-width": "^1.3.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/pods/node_modules/strip-ansi": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"packages/proxy": {
|
||||
"name": "@mariozechner/pi-proxy",
|
||||
"version": "0.7.29",
|
||||
"version": "0.8.0",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.14.0",
|
||||
"hono": "^4.6.16"
|
||||
|
|
@ -5579,7 +5890,7 @@
|
|||
},
|
||||
"packages/tui": {
|
||||
"name": "@mariozechner/pi-tui",
|
||||
"version": "0.7.29",
|
||||
"version": "0.8.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mime-types": "^2.1.4",
|
||||
|
|
@ -5661,7 +5972,7 @@
|
|||
},
|
||||
"packages/web-ui": {
|
||||
"name": "@mariozechner/pi-web-ui",
|
||||
"version": "0.7.29",
|
||||
"version": "0.8.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lmstudio/sdk": "^1.5.0",
|
||||
|
|
@ -5684,6 +5995,109 @@
|
|||
"@mariozechner/mini-lit": "^0.2.0",
|
||||
"lit": "^3.3.1"
|
||||
}
|
||||
},
|
||||
"packages/web-ui/node_modules/@mariozechner/pi-ai": {
|
||||
"version": "0.7.29",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.7.29.tgz",
|
||||
"integrity": "sha512-CtYAyQikG4S2j22+8OBclCBASTMzmyfca32K9SMLIV2W88/1JdJL3B9TfZOdPrp/6WMFRe+TdprXh0UK/I/Ikw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.61.0",
|
||||
"@google/genai": "^1.30.0",
|
||||
"@sinclair/typebox": "^0.34.41",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"chalk": "^5.6.2",
|
||||
"openai": "5.21.0",
|
||||
"partial-json": "^0.1.7",
|
||||
"zod-to-json-schema": "^3.24.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"packages/web-ui/node_modules/@mariozechner/pi-tui": {
|
||||
"version": "0.7.29",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.7.29.tgz",
|
||||
"integrity": "sha512-vqrjc2NynNj+4y5WVabPQZSBkWw2NIYIz4VSROtqkGQD5ilaalZuHd+oIsAByClFlJv0joaa+Qtwbcvfl1nTiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"chalk": "^5.5.0",
|
||||
"marked": "^15.0.12",
|
||||
"mime-types": "^3.0.1",
|
||||
"string-width": "^8.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"packages/web-ui/node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"packages/web-ui/node_modules/chalk": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"packages/web-ui/node_modules/marked": {
|
||||
"version": "15.0.12",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
||||
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"packages/web-ui/node_modules/string-width": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
|
||||
"integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-east-asian-width": "^1.3.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/web-ui/node_modules/strip-ansi": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@mariozechner/pi-agent",
|
||||
"version": "0.7.29",
|
||||
"version": "0.8.0",
|
||||
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
|
@ -18,8 +18,8 @@
|
|||
"prepublishOnly": "npm run clean && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-ai": "^0.7.29",
|
||||
"@mariozechner/pi-tui": "^0.7.29"
|
||||
"@mariozechner/pi-ai": "^0.8.0",
|
||||
"@mariozechner/pi-tui": "^0.8.0"
|
||||
},
|
||||
"keywords": [
|
||||
"ai",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@mariozechner/pi-ai",
|
||||
"version": "0.7.29",
|
||||
"version": "0.8.0",
|
||||
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.8.0] - 2025-11-21
|
||||
|
||||
### Added
|
||||
|
||||
- **Theme System**: Full theming support with 44 customizable color tokens. Two built-in themes (`dark`, `light`) with auto-detection based on terminal background. Use `/theme` command to select themes interactively. Custom themes in `~/.pi/agent/themes/*.json` support live editing - changes apply immediately when the file is saved. Themes use RGB hex values for consistent rendering across terminals. VS Code users: set `terminal.integrated.minimumContrastRatio` to `1` for proper color rendering. See [Theme Documentation](docs/theme.md) for details.
|
||||
|
||||
## [0.7.29] - 2025-11-20
|
||||
|
||||
### Improved
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ Works on Linux, macOS, and Windows (barely tested, needs Git Bash running in the
|
|||
- [API Keys](#api-keys)
|
||||
- [OAuth Authentication (Optional)](#oauth-authentication-optional)
|
||||
- [Custom Models and Providers](#custom-models-and-providers)
|
||||
- [Themes](#themes)
|
||||
- [Slash Commands](#slash-commands)
|
||||
- [Editor Features](#editor-features)
|
||||
- [Project Context Files](#project-context-files)
|
||||
|
|
@ -284,6 +285,79 @@ If the file contains errors (JSON syntax, schema violations, missing fields), th
|
|||
|
||||
See the configuration structure above. Create `~/.pi/agent/models.json` with your Ollama setup, then use `/model` to select your local models. The agent can also help you write this file if you point it to this README.
|
||||
|
||||
## Themes
|
||||
|
||||
Pi supports customizable color themes for the TUI. Two built-in themes are available: `dark` (default) and `light`.
|
||||
|
||||
### Selecting a Theme
|
||||
|
||||
Use the `/theme` command to interactively select a theme, or edit your settings file:
|
||||
|
||||
```bash
|
||||
# Interactive selector
|
||||
pi
|
||||
/theme
|
||||
|
||||
# Or edit ~/.pi/agent/settings.json
|
||||
{
|
||||
"theme": "dark" # or "light"
|
||||
}
|
||||
```
|
||||
|
||||
On first run, Pi auto-detects your terminal background (dark/light) and selects an appropriate theme.
|
||||
|
||||
### Custom Themes
|
||||
|
||||
Create custom themes in `~/.pi/agent/themes/*.json`. Custom themes support **live editing** - when you select a custom theme, Pi watches the file and automatically reloads when you save changes.
|
||||
|
||||
**Workflow for creating themes:**
|
||||
1. Copy a built-in theme as a starting point:
|
||||
```bash
|
||||
mkdir -p ~/.pi/agent/themes
|
||||
# Copy dark theme
|
||||
cp $(npm root -g)/@mariozechner/pi-coding-agent/dist/theme/dark.json ~/.pi/agent/themes/my-theme.json
|
||||
# Or copy light theme
|
||||
cp $(npm root -g)/@mariozechner/pi-coding-agent/dist/theme/light.json ~/.pi/agent/themes/my-theme.json
|
||||
```
|
||||
2. Use `/theme` to select "my-theme"
|
||||
3. Edit `~/.pi/agent/themes/my-theme.json` - changes apply immediately on save
|
||||
4. Iterate until satisfied (no need to re-select the theme)
|
||||
|
||||
See [Theme Documentation](docs/theme.md) for:
|
||||
- Complete list of 44 color tokens
|
||||
- Theme format and examples
|
||||
- Color value formats (hex, RGB, terminal default)
|
||||
|
||||
Example custom theme:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json",
|
||||
"name": "my-theme",
|
||||
"vars": {
|
||||
"accent": "#00aaff",
|
||||
"muted": "#6c6c6c"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "accent",
|
||||
"muted": "muted",
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### VS Code Terminal Color Issue
|
||||
|
||||
**Important:** VS Code's integrated terminal has a known issue with rendering truecolor (24-bit RGB) values. By default, it applies a "minimum contrast ratio" adjustment that can make colors look washed out or identical.
|
||||
|
||||
To fix this, set the contrast ratio to 1 in VS Code settings:
|
||||
|
||||
1. Open Settings (Cmd/Ctrl + ,)
|
||||
2. Search for: `terminal.integrated.minimumContrastRatio`
|
||||
3. Set to: `1`
|
||||
|
||||
This ensures VS Code renders the exact RGB colors defined in your theme.
|
||||
|
||||
## Slash Commands
|
||||
|
||||
The CLI supports several commands to control its behavior:
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Themes allow you to customize the colors used throughout the coding agent TUI.
|
|||
|
||||
Every theme must define all color tokens. There are no optional colors.
|
||||
|
||||
### Core UI (9 colors)
|
||||
### Core UI (10 colors)
|
||||
|
||||
| Token | Purpose | Examples |
|
||||
|-------|---------|----------|
|
||||
|
|
@ -18,9 +18,10 @@ Every theme must define all color tokens. There are no optional colors.
|
|||
| `error` | Error states | Error messages, diff deletions |
|
||||
| `warning` | Warning states | Warning messages |
|
||||
| `muted` | Secondary/dimmed text | Metadata, descriptions, output |
|
||||
| `dim` | Very dimmed text | Less important info, placeholders |
|
||||
| `text` | Default text color | Main content (usually `""`) |
|
||||
|
||||
### Backgrounds & Content Text (6 colors)
|
||||
### Backgrounds & Content Text (7 colors)
|
||||
|
||||
| Token | Purpose |
|
||||
|-------|---------|
|
||||
|
|
@ -29,14 +30,16 @@ Every theme must define all color tokens. There are no optional colors.
|
|||
| `toolPendingBg` | Tool execution box (pending state) |
|
||||
| `toolSuccessBg` | Tool execution box (success state) |
|
||||
| `toolErrorBg` | Tool execution box (error state) |
|
||||
| `toolText` | Tool execution box text color (all states) |
|
||||
| `toolTitle` | Tool execution title/heading (e.g., `$ command`, `read file.txt`) |
|
||||
| `toolOutput` | Tool execution output text |
|
||||
|
||||
### Markdown (9 colors)
|
||||
### Markdown (10 colors)
|
||||
|
||||
| Token | Purpose |
|
||||
|-------|---------|
|
||||
| `mdHeading` | Heading text (`#`, `##`, etc) |
|
||||
| `mdLink` | Link text and URLs |
|
||||
| `mdLink` | Link text |
|
||||
| `mdLinkUrl` | Link URL (in parentheses) |
|
||||
| `mdCode` | Inline code (backticks) |
|
||||
| `mdCodeBlock` | Code block content |
|
||||
| `mdCodeBlockBorder` | Code block fences (```) |
|
||||
|
|
@ -71,7 +74,21 @@ Future-proofing for syntax highlighting support:
|
|||
| `syntaxOperator` | Operators (`+`, `-`, etc) |
|
||||
| `syntaxPunctuation` | Punctuation (`;`, `,`, etc) |
|
||||
|
||||
**Total: 36 color tokens** (all required)
|
||||
### Thinking Level Borders (5 colors)
|
||||
|
||||
Editor border colors that indicate the current thinking/reasoning level:
|
||||
|
||||
| Token | Purpose |
|
||||
|-------|---------|
|
||||
| `thinkingOff` | Border when thinking is off (most subtle) |
|
||||
| `thinkingMinimal` | Border for minimal thinking |
|
||||
| `thinkingLow` | Border for low thinking |
|
||||
| `thinkingMedium` | Border for medium thinking |
|
||||
| `thinkingHigh` | Border for high thinking (most prominent) |
|
||||
|
||||
These create a visual hierarchy: off → minimal → low → medium → high
|
||||
|
||||
**Total: 44 color tokens** (all required)
|
||||
|
||||
## Theme Format
|
||||
|
||||
|
|
@ -241,7 +258,13 @@ Custom themes are loaded from `~/.pi/agent/themes/*.json`.
|
|||
"syntaxNumber": "#ff00ff",
|
||||
"syntaxType": "#00aaff",
|
||||
"syntaxOperator": "primary",
|
||||
"syntaxPunctuation": "secondary"
|
||||
"syntaxPunctuation": "secondary",
|
||||
|
||||
"thinkingOff": "secondary",
|
||||
"thinkingMinimal": "primary",
|
||||
"thinkingLow": "#00aaff",
|
||||
"thinkingMedium": "#00ffff",
|
||||
"thinkingHigh": "#ff00ff"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@mariozechner/pi-coding-agent",
|
||||
"version": "0.7.29",
|
||||
"version": "0.8.0",
|
||||
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
|
@ -22,8 +22,8 @@
|
|||
"prepublishOnly": "npm run clean && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-agent": "^0.7.29",
|
||||
"@mariozechner/pi-ai": "^0.7.29",
|
||||
"@mariozechner/pi-agent": "^0.8.0",
|
||||
"@mariozechner/pi-ai": "^0.8.0",
|
||||
"chalk": "^5.5.0",
|
||||
"diff": "^8.0.2",
|
||||
"glob": "^11.0.3"
|
||||
|
|
|
|||
|
|
@ -236,8 +236,8 @@ Guidelines:
|
|||
- When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did
|
||||
|
||||
Documentation:
|
||||
- Your own documentation (including custom model setup) is at: ${readmePath}
|
||||
- Read it when users ask about features, configuration, or setup, and especially if the user asks you to add a custom model or provider.`;
|
||||
- Your own documentation (including custom model setup and theme creation) is at: ${readmePath}
|
||||
- Read it when users ask about features, configuration, or setup, and especially if the user asks you to add a custom model or provider, or create a custom theme.`;
|
||||
|
||||
// Append project context files
|
||||
const contextFiles = loadProjectContextFiles();
|
||||
|
|
|
|||
|
|
@ -3,19 +3,21 @@
|
|||
"name": "dark",
|
||||
"vars": {
|
||||
"cyan": "#00d7ff",
|
||||
"blue": "#0087ff",
|
||||
"green": "#00ff00",
|
||||
"red": "#ff0000",
|
||||
"blue": "#5f87ff",
|
||||
"green": "#b5bd68",
|
||||
"red": "#cc6666",
|
||||
"yellow": "#ffff00",
|
||||
"gray": 242,
|
||||
"darkGray": 238,
|
||||
"gray": "#808080",
|
||||
"dimGray": "#666666",
|
||||
"darkGray": "#303030",
|
||||
"accent": "#8abeb7",
|
||||
"userMsgBg": "#343541",
|
||||
"toolPendingBg": "#282832",
|
||||
"toolSuccessBg": "#283228",
|
||||
"toolErrorBg": "#3c2828"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "cyan",
|
||||
"accent": "accent",
|
||||
"border": "blue",
|
||||
"borderAccent": "cyan",
|
||||
"borderMuted": "darkGray",
|
||||
|
|
@ -23,6 +25,7 @@
|
|||
"error": "red",
|
||||
"warning": "yellow",
|
||||
"muted": "gray",
|
||||
"dim": "dimGray",
|
||||
"text": "",
|
||||
|
||||
"userMessageBg": "userMsgBg",
|
||||
|
|
@ -30,17 +33,19 @@
|
|||
"toolPendingBg": "toolPendingBg",
|
||||
"toolSuccessBg": "toolSuccessBg",
|
||||
"toolErrorBg": "toolErrorBg",
|
||||
"toolText": "",
|
||||
"toolTitle": "",
|
||||
"toolOutput": "gray",
|
||||
|
||||
"mdHeading": "cyan",
|
||||
"mdLink": "blue",
|
||||
"mdCode": "cyan",
|
||||
"mdCodeBlock": "",
|
||||
"mdHeading": "#f0c674",
|
||||
"mdLink": "#81a2be",
|
||||
"mdLinkUrl": "dimGray",
|
||||
"mdCode": "accent",
|
||||
"mdCodeBlock": "green",
|
||||
"mdCodeBlockBorder": "gray",
|
||||
"mdQuote": "gray",
|
||||
"mdQuoteBorder": "gray",
|
||||
"mdHr": "gray",
|
||||
"mdListBullet": "cyan",
|
||||
"mdListBullet": "accent",
|
||||
|
||||
"toolDiffAdded": "green",
|
||||
"toolDiffRemoved": "red",
|
||||
|
|
@ -54,6 +59,12 @@
|
|||
"syntaxNumber": "yellow",
|
||||
"syntaxType": "cyan",
|
||||
"syntaxOperator": "",
|
||||
"syntaxPunctuation": "gray"
|
||||
"syntaxPunctuation": "gray",
|
||||
|
||||
"thinkingOff": "darkGray",
|
||||
"thinkingMinimal": "#4e4e4e",
|
||||
"thinkingLow": "#5f87af",
|
||||
"thinkingMedium": "#81a2be",
|
||||
"thinkingHigh": "#b294bb"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,27 +2,29 @@
|
|||
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json",
|
||||
"name": "light",
|
||||
"vars": {
|
||||
"darkCyan": "#008899",
|
||||
"darkBlue": "#0066cc",
|
||||
"darkGreen": "#008800",
|
||||
"darkRed": "#cc0000",
|
||||
"darkYellow": "#aa8800",
|
||||
"mediumGray": 242,
|
||||
"lightGray": 250,
|
||||
"teal": "#5f8787",
|
||||
"blue": "#5f87af",
|
||||
"green": "#87af87",
|
||||
"red": "#af5f5f",
|
||||
"yellow": "#d7af5f",
|
||||
"mediumGray": "#6c6c6c",
|
||||
"dimGray": "#8a8a8a",
|
||||
"lightGray": "#b0b0b0",
|
||||
"userMsgBg": "#e8e8e8",
|
||||
"toolPendingBg": "#e8e8f0",
|
||||
"toolSuccessBg": "#e8f0e8",
|
||||
"toolErrorBg": "#f0e8e8"
|
||||
},
|
||||
"colors": {
|
||||
"accent": "darkCyan",
|
||||
"border": "darkBlue",
|
||||
"borderAccent": "darkCyan",
|
||||
"accent": "teal",
|
||||
"border": "blue",
|
||||
"borderAccent": "teal",
|
||||
"borderMuted": "lightGray",
|
||||
"success": "darkGreen",
|
||||
"error": "darkRed",
|
||||
"warning": "darkYellow",
|
||||
"success": "green",
|
||||
"error": "red",
|
||||
"warning": "yellow",
|
||||
"muted": "mediumGray",
|
||||
"dim": "dimGray",
|
||||
"text": "",
|
||||
|
||||
"userMessageBg": "userMsgBg",
|
||||
|
|
@ -30,30 +32,38 @@
|
|||
"toolPendingBg": "toolPendingBg",
|
||||
"toolSuccessBg": "toolSuccessBg",
|
||||
"toolErrorBg": "toolErrorBg",
|
||||
"toolText": "",
|
||||
"toolTitle": "",
|
||||
"toolOutput": "mediumGray",
|
||||
|
||||
"mdHeading": "darkCyan",
|
||||
"mdLink": "darkBlue",
|
||||
"mdCode": "darkCyan",
|
||||
"mdCodeBlock": "",
|
||||
"mdHeading": "yellow",
|
||||
"mdLink": "blue",
|
||||
"mdLinkUrl": "dimGray",
|
||||
"mdCode": "teal",
|
||||
"mdCodeBlock": "green",
|
||||
"mdCodeBlockBorder": "mediumGray",
|
||||
"mdQuote": "mediumGray",
|
||||
"mdQuoteBorder": "mediumGray",
|
||||
"mdHr": "mediumGray",
|
||||
"mdListBullet": "darkCyan",
|
||||
"mdListBullet": "green",
|
||||
|
||||
"toolDiffAdded": "darkGreen",
|
||||
"toolDiffRemoved": "darkRed",
|
||||
"toolDiffAdded": "green",
|
||||
"toolDiffRemoved": "red",
|
||||
"toolDiffContext": "mediumGray",
|
||||
|
||||
"syntaxComment": "mediumGray",
|
||||
"syntaxKeyword": "darkCyan",
|
||||
"syntaxFunction": "darkBlue",
|
||||
"syntaxKeyword": "teal",
|
||||
"syntaxFunction": "blue",
|
||||
"syntaxVariable": "",
|
||||
"syntaxString": "darkGreen",
|
||||
"syntaxNumber": "darkYellow",
|
||||
"syntaxType": "darkCyan",
|
||||
"syntaxString": "green",
|
||||
"syntaxNumber": "yellow",
|
||||
"syntaxType": "teal",
|
||||
"syntaxOperator": "",
|
||||
"syntaxPunctuation": "mediumGray"
|
||||
"syntaxPunctuation": "mediumGray",
|
||||
|
||||
"thinkingOff": "lightGray",
|
||||
"thinkingMinimal": "#9e9e9e",
|
||||
"thinkingLow": "#5f87af",
|
||||
"thinkingMedium": "#5f8787",
|
||||
"thinkingHigh": "#875f87"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
"error",
|
||||
"warning",
|
||||
"muted",
|
||||
"dim",
|
||||
"text",
|
||||
"userMessageBg",
|
||||
"userMessageText",
|
||||
|
|
@ -105,6 +106,10 @@
|
|||
"$ref": "#/$defs/colorValue",
|
||||
"description": "Secondary/dimmed text"
|
||||
},
|
||||
"dim": {
|
||||
"$ref": "#/$defs/colorValue",
|
||||
"description": "Very dimmed text (more subtle than muted)"
|
||||
},
|
||||
"text": {
|
||||
"$ref": "#/$defs/colorValue",
|
||||
"description": "Default text color (usually empty string)"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { MarkdownTheme } from "@mariozechner/pi-tui";
|
||||
import type { EditorTheme, MarkdownTheme, SelectListTheme } from "@mariozechner/pi-tui";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import { TypeCompiler } from "@sinclair/typebox/compiler";
|
||||
import chalk from "chalk";
|
||||
|
|
@ -25,7 +25,7 @@ const ThemeJsonSchema = Type.Object({
|
|||
name: Type.String(),
|
||||
vars: Type.Optional(Type.Record(Type.String(), ColorValueSchema)),
|
||||
colors: Type.Object({
|
||||
// Core UI (9 colors)
|
||||
// Core UI (10 colors)
|
||||
accent: ColorValueSchema,
|
||||
border: ColorValueSchema,
|
||||
borderAccent: ColorValueSchema,
|
||||
|
|
@ -34,17 +34,20 @@ const ThemeJsonSchema = Type.Object({
|
|||
error: ColorValueSchema,
|
||||
warning: ColorValueSchema,
|
||||
muted: ColorValueSchema,
|
||||
dim: ColorValueSchema,
|
||||
text: ColorValueSchema,
|
||||
// Backgrounds & Content Text (6 colors)
|
||||
// Backgrounds & Content Text (7 colors)
|
||||
userMessageBg: ColorValueSchema,
|
||||
userMessageText: ColorValueSchema,
|
||||
toolPendingBg: ColorValueSchema,
|
||||
toolSuccessBg: ColorValueSchema,
|
||||
toolErrorBg: ColorValueSchema,
|
||||
toolText: ColorValueSchema,
|
||||
// Markdown (9 colors)
|
||||
toolTitle: ColorValueSchema,
|
||||
toolOutput: ColorValueSchema,
|
||||
// Markdown (10 colors)
|
||||
mdHeading: ColorValueSchema,
|
||||
mdLink: ColorValueSchema,
|
||||
mdLinkUrl: ColorValueSchema,
|
||||
mdCode: ColorValueSchema,
|
||||
mdCodeBlock: ColorValueSchema,
|
||||
mdCodeBlockBorder: ColorValueSchema,
|
||||
|
|
@ -66,6 +69,12 @@ const ThemeJsonSchema = Type.Object({
|
|||
syntaxType: ColorValueSchema,
|
||||
syntaxOperator: ColorValueSchema,
|
||||
syntaxPunctuation: ColorValueSchema,
|
||||
// Thinking Level Borders (5 colors)
|
||||
thinkingOff: ColorValueSchema,
|
||||
thinkingMinimal: ColorValueSchema,
|
||||
thinkingLow: ColorValueSchema,
|
||||
thinkingMedium: ColorValueSchema,
|
||||
thinkingHigh: ColorValueSchema,
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -82,11 +91,14 @@ export type ThemeColor =
|
|||
| "error"
|
||||
| "warning"
|
||||
| "muted"
|
||||
| "dim"
|
||||
| "text"
|
||||
| "userMessageText"
|
||||
| "toolText"
|
||||
| "toolTitle"
|
||||
| "toolOutput"
|
||||
| "mdHeading"
|
||||
| "mdLink"
|
||||
| "mdLinkUrl"
|
||||
| "mdCode"
|
||||
| "mdCodeBlock"
|
||||
| "mdCodeBlockBorder"
|
||||
|
|
@ -105,7 +117,12 @@ export type ThemeColor =
|
|||
| "syntaxNumber"
|
||||
| "syntaxType"
|
||||
| "syntaxOperator"
|
||||
| "syntaxPunctuation";
|
||||
| "syntaxPunctuation"
|
||||
| "thinkingOff"
|
||||
| "thinkingMinimal"
|
||||
| "thinkingLow"
|
||||
| "thinkingMedium"
|
||||
| "thinkingHigh";
|
||||
|
||||
export type ThemeBg = "userMessageBg" | "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
|
||||
|
||||
|
|
@ -216,8 +233,6 @@ function resolveThemeColors<T extends Record<string, ColorValue>>(
|
|||
// Theme Class
|
||||
// ============================================================================
|
||||
|
||||
const RESET = "\x1b[0m";
|
||||
|
||||
export class Theme {
|
||||
private fgColors: Map<ThemeColor, string>;
|
||||
private bgColors: Map<ThemeBg, string>;
|
||||
|
|
@ -242,13 +257,13 @@ export class Theme {
|
|||
fg(color: ThemeColor, text: string): string {
|
||||
const ansi = this.fgColors.get(color);
|
||||
if (!ansi) throw new Error(`Unknown theme color: ${color}`);
|
||||
return `${ansi}${text}${RESET}`;
|
||||
return `${ansi}${text}\x1b[39m`; // Reset only foreground color
|
||||
}
|
||||
|
||||
bg(color: ThemeBg, text: string): string {
|
||||
const ansi = this.bgColors.get(color);
|
||||
if (!ansi) throw new Error(`Unknown theme background color: ${color}`);
|
||||
return `${ansi}${text}${RESET}`;
|
||||
return `${ansi}${text}\x1b[49m`; // Reset only background color
|
||||
}
|
||||
|
||||
bold(text: string): string {
|
||||
|
|
@ -278,6 +293,24 @@ export class Theme {
|
|||
getColorMode(): ColorMode {
|
||||
return this.mode;
|
||||
}
|
||||
|
||||
getThinkingBorderColor(level: "off" | "minimal" | "low" | "medium" | "high"): (str: string) => string {
|
||||
// Map thinking levels to dedicated theme colors
|
||||
switch (level) {
|
||||
case "off":
|
||||
return (str: string) => this.fg("thinkingOff", str);
|
||||
case "minimal":
|
||||
return (str: string) => this.fg("thinkingMinimal", str);
|
||||
case "low":
|
||||
return (str: string) => this.fg("thinkingLow", str);
|
||||
case "medium":
|
||||
return (str: string) => this.fg("thinkingMedium", str);
|
||||
case "high":
|
||||
return (str: string) => this.fg("thinkingHigh", str);
|
||||
default:
|
||||
return (str: string) => this.fg("thinkingOff", str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -369,7 +402,8 @@ function detectTerminalBackground(): "dark" | "light" {
|
|||
if (parts.length >= 2) {
|
||||
const bg = parseInt(parts[1], 10);
|
||||
if (!Number.isNaN(bg)) {
|
||||
return bg < 8 ? "dark" : "light";
|
||||
const result = bg < 8 ? "dark" : "light";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -385,14 +419,109 @@ function getDefaultTheme(): string {
|
|||
// ============================================================================
|
||||
|
||||
export let theme: Theme;
|
||||
let currentThemeName: string | undefined;
|
||||
let themeWatcher: fs.FSWatcher | undefined;
|
||||
let onThemeChangeCallback: (() => void) | undefined;
|
||||
|
||||
export function initTheme(themeName?: string): void {
|
||||
const name = themeName ?? getDefaultTheme();
|
||||
theme = loadTheme(name);
|
||||
currentThemeName = name;
|
||||
try {
|
||||
theme = loadTheme(name);
|
||||
startThemeWatcher();
|
||||
} catch (error) {
|
||||
// Theme is invalid - fall back to dark theme silently
|
||||
currentThemeName = "dark";
|
||||
theme = loadTheme("dark");
|
||||
// Don't start watcher for fallback theme
|
||||
}
|
||||
}
|
||||
|
||||
export function setTheme(name: string): void {
|
||||
theme = loadTheme(name);
|
||||
export function setTheme(name: string): { success: boolean; error?: string } {
|
||||
currentThemeName = name;
|
||||
try {
|
||||
theme = loadTheme(name);
|
||||
startThemeWatcher();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
// Theme is invalid - fall back to dark theme
|
||||
currentThemeName = "dark";
|
||||
theme = loadTheme("dark");
|
||||
// Don't start watcher for fallback theme
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function onThemeChange(callback: () => void): void {
|
||||
onThemeChangeCallback = callback;
|
||||
}
|
||||
|
||||
function startThemeWatcher(): void {
|
||||
// Stop existing watcher if any
|
||||
if (themeWatcher) {
|
||||
themeWatcher.close();
|
||||
themeWatcher = undefined;
|
||||
}
|
||||
|
||||
// Only watch if it's a custom theme (not built-in)
|
||||
if (!currentThemeName || currentThemeName === "dark" || currentThemeName === "light") {
|
||||
return;
|
||||
}
|
||||
|
||||
const themesDir = getThemesDir();
|
||||
const themeFile = path.join(themesDir, `${currentThemeName}.json`);
|
||||
|
||||
// Only watch if the file exists
|
||||
if (!fs.existsSync(themeFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
themeWatcher = fs.watch(themeFile, (eventType) => {
|
||||
if (eventType === "change") {
|
||||
// Debounce rapid changes
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// Reload the theme
|
||||
theme = loadTheme(currentThemeName!);
|
||||
// Notify callback (to invalidate UI)
|
||||
if (onThemeChangeCallback) {
|
||||
onThemeChangeCallback();
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors (file might be in invalid state while being edited)
|
||||
}
|
||||
}, 100);
|
||||
} else if (eventType === "rename") {
|
||||
// File was deleted or renamed - fall back to default theme
|
||||
setTimeout(() => {
|
||||
if (!fs.existsSync(themeFile)) {
|
||||
currentThemeName = "dark";
|
||||
theme = loadTheme("dark");
|
||||
if (themeWatcher) {
|
||||
themeWatcher.close();
|
||||
themeWatcher = undefined;
|
||||
}
|
||||
if (onThemeChangeCallback) {
|
||||
onThemeChangeCallback();
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
// Ignore errors starting watcher
|
||||
}
|
||||
}
|
||||
|
||||
export function stopThemeWatcher(): void {
|
||||
if (themeWatcher) {
|
||||
themeWatcher.close();
|
||||
themeWatcher = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -403,6 +532,7 @@ export function getMarkdownTheme(): MarkdownTheme {
|
|||
return {
|
||||
heading: (text: string) => theme.fg("mdHeading", text),
|
||||
link: (text: string) => theme.fg("mdLink", text),
|
||||
linkUrl: (text: string) => theme.fg("mdLinkUrl", text),
|
||||
code: (text: string) => theme.fg("mdCode", text),
|
||||
codeBlock: (text: string) => theme.fg("mdCodeBlock", text),
|
||||
codeBlockBorder: (text: string) => theme.fg("mdCodeBlockBorder", text),
|
||||
|
|
@ -410,5 +540,26 @@ export function getMarkdownTheme(): MarkdownTheme {
|
|||
quoteBorder: (text: string) => theme.fg("mdQuoteBorder", text),
|
||||
hr: (text: string) => theme.fg("mdHr", text),
|
||||
listBullet: (text: string) => theme.fg("mdListBullet", text),
|
||||
bold: (text: string) => theme.bold(text),
|
||||
italic: (text: string) => theme.italic(text),
|
||||
underline: (text: string) => theme.underline(text),
|
||||
strikethrough: (text: string) => chalk.strikethrough(text),
|
||||
};
|
||||
}
|
||||
|
||||
export function getSelectListTheme(): SelectListTheme {
|
||||
return {
|
||||
selectedPrefix: (text: string) => theme.fg("accent", text),
|
||||
selectedText: (text: string) => theme.fg("accent", text),
|
||||
description: (text: string) => theme.fg("muted", text),
|
||||
scrollInfo: (text: string) => theme.fg("muted", text),
|
||||
noMatch: (text: string) => theme.fg("muted", text),
|
||||
};
|
||||
}
|
||||
|
||||
export function getEditorTheme(): EditorTheme {
|
||||
return {
|
||||
borderColor: (text: string) => theme.fg("borderMuted", text),
|
||||
selectList: getSelectListTheme(),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
||||
import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
||||
|
||||
/**
|
||||
* Component that renders a complete assistant message
|
||||
|
|
@ -38,13 +38,13 @@ export class AssistantMessageComponent extends Container {
|
|||
if (content.type === "text" && content.text.trim()) {
|
||||
// Assistant text messages with no background - trim the text
|
||||
// Set paddingY=0 to avoid extra spacing before tool executions
|
||||
this.contentContainer.addChild(new Markdown(content.text.trim(), 1, 0));
|
||||
this.contentContainer.addChild(new Markdown(content.text.trim(), 1, 0, getMarkdownTheme()));
|
||||
} else if (content.type === "thinking" && content.thinking.trim()) {
|
||||
// Thinking traces in dark gray italic
|
||||
// Thinking traces in muted color, italic
|
||||
// Use Markdown component with default text style for consistent styling
|
||||
this.contentContainer.addChild(
|
||||
new Markdown(content.thinking.trim(), 1, 0, {
|
||||
color: "gray",
|
||||
new Markdown(content.thinking.trim(), 1, 0, getMarkdownTheme(), {
|
||||
color: (text: string) => theme.fg("muted", text),
|
||||
italic: true,
|
||||
}),
|
||||
);
|
||||
|
|
@ -57,11 +57,11 @@ export class AssistantMessageComponent extends Container {
|
|||
const hasToolCalls = message.content.some((c) => c.type === "toolCall");
|
||||
if (!hasToolCalls) {
|
||||
if (message.stopReason === "aborted") {
|
||||
this.contentContainer.addChild(new Text(chalk.red("\nAborted"), 1, 0));
|
||||
this.contentContainer.addChild(new Text(theme.fg("error", "\nAborted"), 1, 0));
|
||||
} else if (message.stopReason === "error") {
|
||||
const errorMsg = message.errorMessage || "Unknown error";
|
||||
this.contentContainer.addChild(new Spacer(1));
|
||||
this.contentContainer.addChild(new Text(chalk.red(`Error: ${errorMsg}`), 1, 0));
|
||||
this.contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Component } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import { theme } from "../theme/theme.js";
|
||||
|
||||
/**
|
||||
* Dynamic border component that adjusts to viewport width
|
||||
|
|
@ -7,10 +7,14 @@ import chalk from "chalk";
|
|||
export class DynamicBorder implements Component {
|
||||
private color: (str: string) => string;
|
||||
|
||||
constructor(color: (str: string) => string = chalk.blue) {
|
||||
constructor(color: (str: string) => string = (str) => theme.fg("border", str)) {
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
// No cached state to invalidate currently
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
return [this.color("─".repeat(Math.max(1, width)))];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import type { AgentState } from "@mariozechner/pi-agent";
|
||||
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
||||
import { visibleWidth } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import { type Component, visibleWidth } from "@mariozechner/pi-tui";
|
||||
import { theme } from "../theme/theme.js";
|
||||
|
||||
/**
|
||||
* Footer component that shows pwd, token stats, and context usage
|
||||
*/
|
||||
export class FooterComponent {
|
||||
export class FooterComponent implements Component {
|
||||
private state: AgentState;
|
||||
|
||||
constructor(state: AgentState) {
|
||||
|
|
@ -17,6 +17,10 @@ export class FooterComponent {
|
|||
this.state = state;
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
// No cached state to invalidate currently
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
// Calculate cumulative usage from all assistant messages
|
||||
let totalInput = 0;
|
||||
|
|
@ -50,7 +54,8 @@ export class FooterComponent {
|
|||
lastAssistantMessage.usage.cacheWrite
|
||||
: 0;
|
||||
const contextWindow = this.state.model?.contextWindow || 0;
|
||||
const contextPercent = contextWindow > 0 ? ((contextTokens / contextWindow) * 100).toFixed(1) : "0.0";
|
||||
const contextPercentValue = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
|
||||
const contextPercent = contextPercentValue.toFixed(1);
|
||||
|
||||
// Format token counts (similar to web-ui)
|
||||
const formatTokens = (count: number): string => {
|
||||
|
|
@ -80,8 +85,18 @@ export class FooterComponent {
|
|||
if (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);
|
||||
if (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);
|
||||
if (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);
|
||||
if (totalCost) statsParts.push(`$${totalCost.toFixed(3)}`);
|
||||
statsParts.push(`${contextPercent}%`);
|
||||
if (totalCost) statsParts.push(`${totalCost.toFixed(3)}`);
|
||||
|
||||
// Colorize context percentage based on usage
|
||||
let contextPercentStr: string;
|
||||
if (contextPercentValue > 90) {
|
||||
contextPercentStr = theme.fg("error", `${contextPercent}%`);
|
||||
} else if (contextPercentValue > 70) {
|
||||
contextPercentStr = theme.fg("warning", `${contextPercent}%`);
|
||||
} else {
|
||||
contextPercentStr = `${contextPercent}%`;
|
||||
}
|
||||
statsParts.push(contextPercentStr);
|
||||
|
||||
const statsLeft = statsParts.join(" ");
|
||||
|
||||
|
|
@ -126,6 +141,6 @@ export class FooterComponent {
|
|||
}
|
||||
|
||||
// Return two lines: pwd and stats
|
||||
return [chalk.gray(pwd), chalk.gray(statsLine)];
|
||||
return [theme.fg("dim", pwd), theme.fg("dim", statsLine)];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import type { Model } from "@mariozechner/pi-ai";
|
||||
import { Container, Input, Spacer, Text, type TUI } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import { getAvailableModels } from "../model-config.js";
|
||||
import type { SettingsManager } from "../settings-manager.js";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
||||
interface ModelItem {
|
||||
provider: string;
|
||||
|
|
@ -42,12 +43,12 @@ export class ModelSelectorComponent extends Container {
|
|||
this.onCancelCallback = onCancel;
|
||||
|
||||
// Add top border
|
||||
this.addChild(new Text(chalk.blue("─".repeat(80)), 0, 0));
|
||||
this.addChild(new DynamicBorder());
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
// Add hint about API key filtering
|
||||
this.addChild(
|
||||
new Text(chalk.yellow("Only showing models with configured API keys (see README for details)"), 0, 0),
|
||||
new Text(theme.fg("warning", "Only showing models with configured API keys (see README for details)"), 0, 0),
|
||||
);
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
|
|
@ -70,7 +71,7 @@ export class ModelSelectorComponent extends Container {
|
|||
this.addChild(new Spacer(1));
|
||||
|
||||
// Add bottom border
|
||||
this.addChild(new Text(chalk.blue("─".repeat(80)), 0, 0));
|
||||
this.addChild(new DynamicBorder());
|
||||
|
||||
// Load models and do initial render
|
||||
this.loadModels().then(() => {
|
||||
|
|
@ -150,15 +151,15 @@ export class ModelSelectorComponent extends Container {
|
|||
|
||||
let line = "";
|
||||
if (isSelected) {
|
||||
const prefix = chalk.blue("→ ");
|
||||
const prefix = theme.fg("accent", "→ ");
|
||||
const modelText = `${item.id}`;
|
||||
const providerBadge = chalk.gray(`[${item.provider}]`);
|
||||
const checkmark = isCurrent ? chalk.green(" ✓") : "";
|
||||
line = prefix + chalk.blue(modelText) + " " + providerBadge + checkmark;
|
||||
const providerBadge = theme.fg("muted", `[${item.provider}]`);
|
||||
const checkmark = isCurrent ? theme.fg("success", " ✓") : "";
|
||||
line = prefix + theme.fg("accent", modelText) + " " + providerBadge + checkmark;
|
||||
} else {
|
||||
const modelText = ` ${item.id}`;
|
||||
const providerBadge = chalk.gray(`[${item.provider}]`);
|
||||
const checkmark = isCurrent ? chalk.green(" ✓") : "";
|
||||
const providerBadge = theme.fg("muted", `[${item.provider}]`);
|
||||
const checkmark = isCurrent ? theme.fg("success", " ✓") : "";
|
||||
line = modelText + " " + providerBadge + checkmark;
|
||||
}
|
||||
|
||||
|
|
@ -167,7 +168,7 @@ export class ModelSelectorComponent extends Container {
|
|||
|
||||
// Add scroll indicator if needed
|
||||
if (startIndex > 0 || endIndex < this.filteredModels.length) {
|
||||
const scrollInfo = chalk.gray(` (${this.selectedIndex + 1}/${this.filteredModels.length})`);
|
||||
const scrollInfo = theme.fg("muted", ` (${this.selectedIndex + 1}/${this.filteredModels.length})`);
|
||||
this.listContainer.addChild(new Text(scrollInfo, 0, 0));
|
||||
}
|
||||
|
||||
|
|
@ -176,10 +177,10 @@ export class ModelSelectorComponent extends Container {
|
|||
// Show error in red
|
||||
const errorLines = this.errorMessage.split("\n");
|
||||
for (const line of errorLines) {
|
||||
this.listContainer.addChild(new Text(chalk.red(line), 0, 0));
|
||||
this.listContainer.addChild(new Text(theme.fg("error", line), 0, 0));
|
||||
}
|
||||
} else if (this.filteredModels.length === 0) {
|
||||
this.listContainer.addChild(new Text(chalk.gray(" No matching models"), 0, 0));
|
||||
this.listContainer.addChild(new Text(theme.fg("muted", " No matching models"), 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Container, Spacer, Text } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import { getOAuthProviders, type OAuthProviderInfo } from "../oauth/index.js";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
||||
/**
|
||||
* Component that renders an OAuth provider selector
|
||||
|
|
@ -24,12 +25,12 @@ export class OAuthSelectorComponent extends Container {
|
|||
this.loadProviders();
|
||||
|
||||
// Add top border
|
||||
this.addChild(new Text(chalk.blue("─".repeat(80)), 0, 0));
|
||||
this.addChild(new DynamicBorder());
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
// Add title
|
||||
const title = mode === "login" ? "Select provider to login:" : "Select provider to logout:";
|
||||
this.addChild(new Text(chalk.bold(title), 0, 0));
|
||||
this.addChild(new Text(theme.bold(title), 0, 0));
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
// Create list container
|
||||
|
|
@ -39,7 +40,7 @@ export class OAuthSelectorComponent extends Container {
|
|||
this.addChild(new Spacer(1));
|
||||
|
||||
// Add bottom border
|
||||
this.addChild(new Text(chalk.blue("─".repeat(80)), 0, 0));
|
||||
this.addChild(new DynamicBorder());
|
||||
|
||||
// Initial render
|
||||
this.updateList();
|
||||
|
|
@ -62,11 +63,11 @@ export class OAuthSelectorComponent extends Container {
|
|||
|
||||
let line = "";
|
||||
if (isSelected) {
|
||||
const prefix = chalk.blue("→ ");
|
||||
const text = isAvailable ? chalk.blue(provider.name) : chalk.dim(provider.name);
|
||||
const prefix = theme.fg("accent", "→ ");
|
||||
const text = isAvailable ? theme.fg("accent", provider.name) : theme.fg("dim", provider.name);
|
||||
line = prefix + text;
|
||||
} else {
|
||||
const text = isAvailable ? ` ${provider.name}` : chalk.dim(` ${provider.name}`);
|
||||
const text = isAvailable ? ` ${provider.name}` : theme.fg("dim", ` ${provider.name}`);
|
||||
line = text;
|
||||
}
|
||||
|
||||
|
|
@ -77,7 +78,7 @@ export class OAuthSelectorComponent extends Container {
|
|||
if (this.allProviders.length === 0) {
|
||||
const message =
|
||||
this.mode === "login" ? "No OAuth providers available" : "No OAuth providers logged in. Use /login first.";
|
||||
this.listContainer.addChild(new Text(chalk.gray(` ${message}`), 0, 0));
|
||||
this.listContainer.addChild(new Text(theme.fg("muted", ` ${message}`), 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,6 @@
|
|||
import { type Component, Container, type SelectItem, SelectList } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
|
||||
/**
|
||||
* Dynamic border component that adjusts to viewport width
|
||||
*/
|
||||
class DynamicBorder implements Component {
|
||||
render(width: number): string[] {
|
||||
return [chalk.blue("─".repeat(Math.max(1, width)))];
|
||||
}
|
||||
}
|
||||
import { Container, type SelectItem, SelectList } from "@mariozechner/pi-tui";
|
||||
import { getSelectListTheme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
||||
/**
|
||||
* Component that renders a queue mode selector with borders
|
||||
|
|
@ -36,7 +28,7 @@ export class QueueModeSelectorComponent extends Container {
|
|||
this.addChild(new DynamicBorder());
|
||||
|
||||
// Create selector
|
||||
this.selectList = new SelectList(queueModes, 2);
|
||||
this.selectList = new SelectList(queueModes, 2, getSelectListTheme());
|
||||
|
||||
// Preselect current mode
|
||||
const currentIndex = queueModes.findIndex((item) => item.value === currentMode);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,7 @@
|
|||
import { type Component, Container, Input, Spacer, Text } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import type { SessionManager } from "../session-manager.js";
|
||||
|
||||
/**
|
||||
* Dynamic border component that adjusts to viewport width
|
||||
*/
|
||||
class DynamicBorder implements Component {
|
||||
render(width: number): string[] {
|
||||
return [chalk.blue("─".repeat(Math.max(1, width)))];
|
||||
}
|
||||
}
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
||||
interface SessionItem {
|
||||
path: string;
|
||||
|
|
@ -67,6 +59,10 @@ class SessionList implements Component {
|
|||
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1));
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
// No cached state to invalidate currently
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
const lines: string[] = [];
|
||||
|
||||
|
|
@ -75,7 +71,7 @@ class SessionList implements Component {
|
|||
lines.push(""); // Blank line after search
|
||||
|
||||
if (this.filteredSessions.length === 0) {
|
||||
lines.push(chalk.gray(" No sessions found"));
|
||||
lines.push(theme.fg("muted", " No sessions found"));
|
||||
return lines;
|
||||
}
|
||||
|
||||
|
|
@ -112,16 +108,16 @@ class SessionList implements Component {
|
|||
const normalizedMessage = session.firstMessage.replace(/\n/g, " ").trim();
|
||||
|
||||
// First line: cursor + message
|
||||
const cursor = isSelected ? chalk.blue("› ") : " ";
|
||||
const cursor = isSelected ? theme.fg("accent", "› ") : " ";
|
||||
const maxMsgWidth = width - 2; // Account for cursor
|
||||
const truncatedMsg = normalizedMessage.substring(0, maxMsgWidth);
|
||||
const messageLine = cursor + (isSelected ? chalk.bold(truncatedMsg) : truncatedMsg);
|
||||
const messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg);
|
||||
|
||||
// Second line: metadata (dimmed)
|
||||
const modified = formatDate(session.modified);
|
||||
const msgCount = `${session.messageCount} message${session.messageCount !== 1 ? "s" : ""}`;
|
||||
const metadata = ` ${modified} · ${msgCount}`;
|
||||
const metadataLine = chalk.dim(metadata);
|
||||
const metadataLine = theme.fg("dim", metadata);
|
||||
|
||||
lines.push(messageLine);
|
||||
lines.push(metadataLine);
|
||||
|
|
@ -130,7 +126,7 @@ class SessionList implements Component {
|
|||
|
||||
// Add scroll indicator if needed
|
||||
if (startIndex > 0 || endIndex < this.filteredSessions.length) {
|
||||
const scrollInfo = chalk.gray(` (${this.selectedIndex + 1}/${this.filteredSessions.length})`);
|
||||
const scrollInfo = theme.fg("muted", ` (${this.selectedIndex + 1}/${this.filteredSessions.length})`);
|
||||
lines.push(scrollInfo);
|
||||
}
|
||||
|
||||
|
|
@ -185,7 +181,7 @@ export class SessionSelectorComponent extends Container {
|
|||
|
||||
// Add header
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(new Text(chalk.bold("Resume Session"), 1, 0));
|
||||
this.addChild(new Text(theme.bold("Resume Session"), 1, 0));
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(new DynamicBorder());
|
||||
this.addChild(new Spacer(1));
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Container, type SelectItem, SelectList } from "@mariozechner/pi-tui";
|
||||
import { getAvailableThemes, theme } from "../theme/theme.js";
|
||||
import { getAvailableThemes, getSelectListTheme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
||||
/**
|
||||
|
|
@ -7,9 +7,16 @@ import { DynamicBorder } from "./dynamic-border.js";
|
|||
*/
|
||||
export class ThemeSelectorComponent extends Container {
|
||||
private selectList: SelectList;
|
||||
private onPreview: (themeName: string) => void;
|
||||
|
||||
constructor(currentTheme: string, onSelect: (themeName: string) => void, onCancel: () => void) {
|
||||
constructor(
|
||||
currentTheme: string,
|
||||
onSelect: (themeName: string) => void,
|
||||
onCancel: () => void,
|
||||
onPreview: (themeName: string) => void,
|
||||
) {
|
||||
super();
|
||||
this.onPreview = onPreview;
|
||||
|
||||
// Get available themes and create select items
|
||||
const themes = getAvailableThemes();
|
||||
|
|
@ -20,10 +27,10 @@ export class ThemeSelectorComponent extends Container {
|
|||
}));
|
||||
|
||||
// Add top border
|
||||
this.addChild(new DynamicBorder((text) => theme.fg("border", text)));
|
||||
this.addChild(new DynamicBorder());
|
||||
|
||||
// Create selector
|
||||
this.selectList = new SelectList(themeItems, 10);
|
||||
this.selectList = new SelectList(themeItems, 10, getSelectListTheme());
|
||||
|
||||
// Preselect current theme
|
||||
const currentIndex = themes.indexOf(currentTheme);
|
||||
|
|
@ -39,10 +46,14 @@ export class ThemeSelectorComponent extends Container {
|
|||
onCancel();
|
||||
};
|
||||
|
||||
this.selectList.onSelectionChange = (item) => {
|
||||
this.onPreview(item.value);
|
||||
};
|
||||
|
||||
this.addChild(this.selectList);
|
||||
|
||||
// Add bottom border
|
||||
this.addChild(new DynamicBorder((text) => theme.fg("border", text)));
|
||||
this.addChild(new DynamicBorder());
|
||||
}
|
||||
|
||||
getSelectList(): SelectList {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,7 @@
|
|||
import type { ThinkingLevel } from "@mariozechner/pi-agent";
|
||||
import { type Component, Container, type SelectItem, SelectList } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
|
||||
/**
|
||||
* Dynamic border component that adjusts to viewport width
|
||||
*/
|
||||
class DynamicBorder implements Component {
|
||||
render(width: number): string[] {
|
||||
return [chalk.blue("─".repeat(Math.max(1, width)))];
|
||||
}
|
||||
}
|
||||
import { Container, type SelectItem, SelectList } from "@mariozechner/pi-tui";
|
||||
import { getSelectListTheme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
||||
/**
|
||||
* Component that renders a thinking level selector with borders
|
||||
|
|
@ -32,7 +24,7 @@ export class ThinkingSelectorComponent extends Container {
|
|||
this.addChild(new DynamicBorder());
|
||||
|
||||
// Create selector
|
||||
this.selectList = new SelectList(thinkingLevels, 5);
|
||||
this.selectList = new SelectList(thinkingLevels, 5, getSelectListTheme());
|
||||
|
||||
// Preselect current level
|
||||
const currentIndex = thinkingLevels.findIndex((item) => item.value === currentLevel);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import * as os from "node:os";
|
||||
import { Container, Spacer, Text } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import * as Diff from "diff";
|
||||
import stripAnsi from "strip-ansi";
|
||||
import { theme } from "../theme/theme.js";
|
||||
|
||||
/**
|
||||
* Convert absolute path to tilde notation if it's in home directory
|
||||
|
|
@ -22,104 +21,6 @@ function replaceTabs(text: string): string {
|
|||
return text.replace(/\t/g, " ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unified diff with line numbers and context
|
||||
*/
|
||||
function generateDiff(oldStr: string, newStr: string): string {
|
||||
const parts = Diff.diffLines(oldStr, newStr);
|
||||
const output: string[] = [];
|
||||
|
||||
// Calculate max line number for padding
|
||||
const oldLines = oldStr.split("\n");
|
||||
const newLines = newStr.split("\n");
|
||||
const maxLineNum = Math.max(oldLines.length, newLines.length);
|
||||
const lineNumWidth = String(maxLineNum).length;
|
||||
|
||||
const CONTEXT_LINES = 2; // Show 2 lines of context around changes
|
||||
|
||||
let oldLineNum = 1;
|
||||
let newLineNum = 1;
|
||||
let lastWasChange = false;
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
const raw = part.value.split("\n");
|
||||
if (raw[raw.length - 1] === "") {
|
||||
raw.pop();
|
||||
}
|
||||
|
||||
if (part.added || part.removed) {
|
||||
// Show the change
|
||||
for (const line of raw) {
|
||||
if (part.added) {
|
||||
const lineNum = String(newLineNum).padStart(lineNumWidth, " ");
|
||||
output.push(chalk.green(`${lineNum} ${line}`));
|
||||
newLineNum++;
|
||||
} else {
|
||||
// removed
|
||||
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
||||
output.push(chalk.red(`${lineNum} ${line}`));
|
||||
oldLineNum++;
|
||||
}
|
||||
}
|
||||
lastWasChange = true;
|
||||
} else {
|
||||
// Context lines - only show a few before/after changes
|
||||
const isFirstPart = i === 0;
|
||||
const isLastPart = i === parts.length - 1;
|
||||
const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
|
||||
|
||||
if (lastWasChange || nextPartIsChange || isFirstPart || isLastPart) {
|
||||
// Show context
|
||||
let linesToShow = raw;
|
||||
let skipStart = 0;
|
||||
let skipEnd = 0;
|
||||
|
||||
if (!isFirstPart && !lastWasChange) {
|
||||
// Show only last N lines as leading context
|
||||
skipStart = Math.max(0, raw.length - CONTEXT_LINES);
|
||||
linesToShow = raw.slice(skipStart);
|
||||
}
|
||||
|
||||
if (!isLastPart && !nextPartIsChange && linesToShow.length > CONTEXT_LINES) {
|
||||
// Show only first N lines as trailing context
|
||||
skipEnd = linesToShow.length - CONTEXT_LINES;
|
||||
linesToShow = linesToShow.slice(0, CONTEXT_LINES);
|
||||
}
|
||||
|
||||
// Add ellipsis if we skipped lines at start
|
||||
if (skipStart > 0) {
|
||||
output.push(chalk.dim(`${"".padStart(lineNumWidth, " ")} ...`));
|
||||
}
|
||||
|
||||
for (const line of linesToShow) {
|
||||
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
||||
output.push(chalk.dim(`${lineNum} ${line}`));
|
||||
oldLineNum++;
|
||||
newLineNum++;
|
||||
}
|
||||
|
||||
// Add ellipsis if we skipped lines at end
|
||||
if (skipEnd > 0) {
|
||||
output.push(chalk.dim(`${"".padStart(lineNumWidth, " ")} ...`));
|
||||
}
|
||||
|
||||
// Update line numbers for skipped lines
|
||||
oldLineNum += skipStart + skipEnd;
|
||||
newLineNum += skipStart + skipEnd;
|
||||
} else {
|
||||
// Skip these context lines entirely
|
||||
oldLineNum += raw.length;
|
||||
newLineNum += raw.length;
|
||||
}
|
||||
|
||||
lastWasChange = false;
|
||||
}
|
||||
}
|
||||
|
||||
return output.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that renders a tool call with its result (updateable)
|
||||
*/
|
||||
|
|
@ -140,7 +41,7 @@ export class ToolExecutionComponent extends Container {
|
|||
this.args = args;
|
||||
this.addChild(new Spacer(1));
|
||||
// Content with colored background and padding
|
||||
this.contentText = new Text("", 1, 1, { r: 40, g: 40, b: 50 });
|
||||
this.contentText = new Text("", 1, 1, (text: string) => theme.bg("toolPendingBg", text));
|
||||
this.addChild(this.contentText);
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
|
@ -165,13 +66,13 @@ export class ToolExecutionComponent extends Container {
|
|||
}
|
||||
|
||||
private updateDisplay(): void {
|
||||
const bgColor = this.result
|
||||
const bgFn = this.result
|
||||
? this.result.isError
|
||||
? { r: 60, g: 40, b: 40 }
|
||||
: { r: 40, g: 50, b: 40 }
|
||||
: { r: 40, g: 40, b: 50 };
|
||||
? (text: string) => theme.bg("toolErrorBg", text)
|
||||
: (text: string) => theme.bg("toolSuccessBg", text)
|
||||
: (text: string) => theme.bg("toolPendingBg", text);
|
||||
|
||||
this.contentText.setCustomBgRgb(bgColor);
|
||||
this.contentText.setCustomBgFn(bgFn);
|
||||
this.contentText.setText(this.formatToolExecution());
|
||||
}
|
||||
|
||||
|
|
@ -200,7 +101,7 @@ export class ToolExecutionComponent extends Container {
|
|||
// Format based on tool type
|
||||
if (this.toolName === "bash") {
|
||||
const command = this.args?.command || "";
|
||||
text = chalk.bold(`$ ${command || chalk.dim("...")}`);
|
||||
text = theme.fg("toolTitle", theme.bold(`$ ${command || theme.fg("toolOutput", "...")}`));
|
||||
|
||||
if (this.result) {
|
||||
// Show output without code fences - more minimal
|
||||
|
|
@ -211,9 +112,9 @@ export class ToolExecutionComponent extends Container {
|
|||
const displayLines = lines.slice(0, maxLines);
|
||||
const remaining = lines.length - maxLines;
|
||||
|
||||
text += "\n\n" + displayLines.map((line: string) => chalk.dim(line)).join("\n");
|
||||
text += "\n\n" + displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n");
|
||||
if (remaining > 0) {
|
||||
text += chalk.dim(`\n... (${remaining} more lines)`);
|
||||
text += theme.fg("toolOutput", `\n... (${remaining} more lines)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -223,13 +124,13 @@ export class ToolExecutionComponent extends Container {
|
|||
const limit = this.args?.limit;
|
||||
|
||||
// Build path display with offset/limit suffix
|
||||
let pathDisplay = path ? chalk.cyan(path) : chalk.dim("...");
|
||||
let pathDisplay = path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
|
||||
if (offset !== undefined) {
|
||||
const endLine = limit !== undefined ? offset + limit : "";
|
||||
pathDisplay += chalk.dim(`:${offset}${endLine ? `-${endLine}` : ""}`);
|
||||
pathDisplay += theme.fg("toolOutput", `:${offset}${endLine ? `-${endLine}` : ""}`);
|
||||
}
|
||||
|
||||
text = chalk.bold("read") + " " + pathDisplay;
|
||||
text = theme.fg("toolTitle", theme.bold("read")) + " " + pathDisplay;
|
||||
|
||||
if (this.result) {
|
||||
const output = this.getTextOutput();
|
||||
|
|
@ -238,9 +139,9 @@ export class ToolExecutionComponent extends Container {
|
|||
const displayLines = lines.slice(0, maxLines);
|
||||
const remaining = lines.length - maxLines;
|
||||
|
||||
text += "\n\n" + displayLines.map((line: string) => chalk.dim(replaceTabs(line))).join("\n");
|
||||
text += "\n\n" + displayLines.map((line: string) => theme.fg("toolOutput", replaceTabs(line))).join("\n");
|
||||
if (remaining > 0) {
|
||||
text += chalk.dim(`\n... (${remaining} more lines)`);
|
||||
text += theme.fg("toolOutput", `\n... (${remaining} more lines)`);
|
||||
}
|
||||
}
|
||||
} else if (this.toolName === "write") {
|
||||
|
|
@ -249,7 +150,10 @@ export class ToolExecutionComponent extends Container {
|
|||
const lines = fileContent ? fileContent.split("\n") : [];
|
||||
const totalLines = lines.length;
|
||||
|
||||
text = chalk.bold("write") + " " + (path ? chalk.cyan(path) : chalk.dim("..."));
|
||||
text =
|
||||
theme.fg("toolTitle", theme.bold("write")) +
|
||||
" " +
|
||||
(path ? theme.fg("accent", path) : theme.fg("toolOutput", "..."));
|
||||
if (totalLines > 10) {
|
||||
text += ` (${totalLines} lines)`;
|
||||
}
|
||||
|
|
@ -260,32 +164,35 @@ export class ToolExecutionComponent extends Container {
|
|||
const displayLines = lines.slice(0, maxLines);
|
||||
const remaining = lines.length - maxLines;
|
||||
|
||||
text += "\n\n" + displayLines.map((line: string) => chalk.dim(replaceTabs(line))).join("\n");
|
||||
text += "\n\n" + displayLines.map((line: string) => theme.fg("toolOutput", replaceTabs(line))).join("\n");
|
||||
if (remaining > 0) {
|
||||
text += chalk.dim(`\n... (${remaining} more lines)`);
|
||||
text += theme.fg("toolOutput", `\n... (${remaining} more lines)`);
|
||||
}
|
||||
}
|
||||
} else if (this.toolName === "edit") {
|
||||
const path = shortenPath(this.args?.file_path || this.args?.path || "");
|
||||
text = chalk.bold("edit") + " " + (path ? chalk.cyan(path) : chalk.dim("..."));
|
||||
text =
|
||||
theme.fg("toolTitle", theme.bold("edit")) +
|
||||
" " +
|
||||
(path ? theme.fg("accent", path) : theme.fg("toolOutput", "..."));
|
||||
|
||||
if (this.result) {
|
||||
// Show error message if it's an error
|
||||
if (this.result.isError) {
|
||||
const errorText = this.getTextOutput();
|
||||
if (errorText) {
|
||||
text += "\n\n" + chalk.red(errorText);
|
||||
text += "\n\n" + theme.fg("error", errorText);
|
||||
}
|
||||
} else if (this.result.details?.diff) {
|
||||
// Show diff if available
|
||||
const diffLines = this.result.details.diff.split("\n");
|
||||
const coloredLines = diffLines.map((line: string) => {
|
||||
if (line.startsWith("+")) {
|
||||
return chalk.green(line);
|
||||
return theme.fg("toolDiffAdded", line);
|
||||
} else if (line.startsWith("-")) {
|
||||
return chalk.red(line);
|
||||
return theme.fg("toolDiffRemoved", line);
|
||||
} else {
|
||||
return chalk.dim(line);
|
||||
return theme.fg("toolDiffContext", line);
|
||||
}
|
||||
});
|
||||
text += "\n\n" + coloredLines.join("\n");
|
||||
|
|
@ -293,7 +200,7 @@ export class ToolExecutionComponent extends Container {
|
|||
}
|
||||
} else {
|
||||
// Generic tool
|
||||
text = chalk.bold(this.toolName);
|
||||
text = theme.fg("toolTitle", theme.bold(this.toolName));
|
||||
|
||||
const content = JSON.stringify(this.args, null, 2);
|
||||
text += "\n\n" + content;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
TruncatedText,
|
||||
TUI,
|
||||
} from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
|
||||
import { exec } from "child_process";
|
||||
import { getChangelogPath, parseChangelog } from "../changelog.js";
|
||||
import { exportSessionToHtml } from "../export-html.js";
|
||||
|
|
@ -21,7 +21,7 @@ import { getApiKeyForModel, getAvailableModels } from "../model-config.js";
|
|||
import { listOAuthProviders, login, logout } from "../oauth/index.js";
|
||||
import type { SessionManager } from "../session-manager.js";
|
||||
import type { SettingsManager } from "../settings-manager.js";
|
||||
import { setTheme } from "../theme/theme.js";
|
||||
import { getEditorTheme, getMarkdownTheme, onThemeChange, setTheme, theme } from "../theme/theme.js";
|
||||
import { AssistantMessageComponent } from "./assistant-message.js";
|
||||
import { CustomEditor } from "./custom-editor.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
|
@ -114,7 +114,7 @@ export class TuiRenderer {
|
|||
this.chatContainer = new Container();
|
||||
this.pendingMessagesContainer = new Container();
|
||||
this.statusContainer = new Container();
|
||||
this.editor = new CustomEditor();
|
||||
this.editor = new CustomEditor(getEditorTheme());
|
||||
this.editorContainer = new Container(); // Container to hold editor or selector
|
||||
this.editorContainer.addChild(this.editor); // Start with editor
|
||||
this.footer = new FooterComponent(agent.state);
|
||||
|
|
@ -193,34 +193,34 @@ export class TuiRenderer {
|
|||
if (this.isInitialized) return;
|
||||
|
||||
// Add header with logo and instructions
|
||||
const logo = chalk.bold.cyan("pi") + chalk.dim(` v${this.version}`);
|
||||
const logo = theme.bold(theme.fg("accent", "pi")) + theme.fg("dim", ` v${this.version}`);
|
||||
const instructions =
|
||||
chalk.dim("esc") +
|
||||
chalk.gray(" to interrupt") +
|
||||
theme.fg("dim", "esc") +
|
||||
theme.fg("muted", " to interrupt") +
|
||||
"\n" +
|
||||
chalk.dim("ctrl+c") +
|
||||
chalk.gray(" to clear") +
|
||||
theme.fg("dim", "ctrl+c") +
|
||||
theme.fg("muted", " to clear") +
|
||||
"\n" +
|
||||
chalk.dim("ctrl+c twice") +
|
||||
chalk.gray(" to exit") +
|
||||
theme.fg("dim", "ctrl+c twice") +
|
||||
theme.fg("muted", " to exit") +
|
||||
"\n" +
|
||||
chalk.dim("ctrl+k") +
|
||||
chalk.gray(" to delete line") +
|
||||
theme.fg("dim", "ctrl+k") +
|
||||
theme.fg("muted", " to delete line") +
|
||||
"\n" +
|
||||
chalk.dim("shift+tab") +
|
||||
chalk.gray(" to cycle thinking") +
|
||||
theme.fg("dim", "shift+tab") +
|
||||
theme.fg("muted", " to cycle thinking") +
|
||||
"\n" +
|
||||
chalk.dim("ctrl+p") +
|
||||
chalk.gray(" to cycle models") +
|
||||
theme.fg("dim", "ctrl+p") +
|
||||
theme.fg("muted", " to cycle models") +
|
||||
"\n" +
|
||||
chalk.dim("ctrl+o") +
|
||||
chalk.gray(" to expand tools") +
|
||||
theme.fg("dim", "ctrl+o") +
|
||||
theme.fg("muted", " to expand tools") +
|
||||
"\n" +
|
||||
chalk.dim("/") +
|
||||
chalk.gray(" for commands") +
|
||||
theme.fg("dim", "/") +
|
||||
theme.fg("muted", " for commands") +
|
||||
"\n" +
|
||||
chalk.dim("drop files") +
|
||||
chalk.gray(" to attach");
|
||||
theme.fg("dim", "drop files") +
|
||||
theme.fg("muted", " to attach");
|
||||
const header = new Text(logo + "\n" + instructions, 1, 0);
|
||||
|
||||
// Setup UI layout
|
||||
|
|
@ -230,28 +230,28 @@ export class TuiRenderer {
|
|||
|
||||
// Add new version notification if available
|
||||
if (this.newVersion) {
|
||||
this.ui.addChild(new DynamicBorder(chalk.yellow));
|
||||
this.ui.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
||||
this.ui.addChild(
|
||||
new Text(
|
||||
chalk.bold.yellow("Update Available") +
|
||||
theme.bold(theme.fg("warning", "Update Available")) +
|
||||
"\n" +
|
||||
chalk.gray(`New version ${this.newVersion} is available. Run: `) +
|
||||
chalk.cyan("npm install -g @mariozechner/pi-coding-agent"),
|
||||
theme.fg("muted", `New version ${this.newVersion} is available. Run: `) +
|
||||
theme.fg("accent", "npm install -g @mariozechner/pi-coding-agent"),
|
||||
1,
|
||||
0,
|
||||
),
|
||||
);
|
||||
this.ui.addChild(new DynamicBorder(chalk.yellow));
|
||||
this.ui.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
||||
}
|
||||
|
||||
// Add changelog if provided
|
||||
if (this.changelogMarkdown) {
|
||||
this.ui.addChild(new DynamicBorder(chalk.cyan));
|
||||
this.ui.addChild(new Text(chalk.bold.cyan("What's New"), 1, 0));
|
||||
this.ui.addChild(new DynamicBorder());
|
||||
this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
|
||||
this.ui.addChild(new Spacer(1));
|
||||
this.ui.addChild(new Markdown(this.changelogMarkdown.trim(), 1, 0));
|
||||
this.ui.addChild(new Markdown(this.changelogMarkdown.trim(), 1, 0, getMarkdownTheme()));
|
||||
this.ui.addChild(new Spacer(1));
|
||||
this.ui.addChild(new DynamicBorder(chalk.cyan));
|
||||
this.ui.addChild(new DynamicBorder());
|
||||
}
|
||||
|
||||
this.ui.addChild(this.chatContainer);
|
||||
|
|
@ -435,6 +435,13 @@ export class TuiRenderer {
|
|||
// Start the UI
|
||||
this.ui.start();
|
||||
this.isInitialized = true;
|
||||
|
||||
// Set up theme file watcher for live reload
|
||||
onThemeChange(() => {
|
||||
this.ui.invalidate();
|
||||
this.updateEditorBorderColor();
|
||||
this.ui.requestRender();
|
||||
});
|
||||
}
|
||||
|
||||
async handleEvent(event: AgentEvent, state: AgentState): Promise<void> {
|
||||
|
|
@ -454,7 +461,12 @@ export class TuiRenderer {
|
|||
this.loadingAnimation.stop();
|
||||
}
|
||||
this.statusContainer.clear();
|
||||
this.loadingAnimation = new Loader(this.ui, "Working... (esc to interrupt)");
|
||||
this.loadingAnimation = new Loader(
|
||||
this.ui,
|
||||
(spinner) => theme.fg("accent", spinner),
|
||||
(text) => theme.fg("muted", text),
|
||||
"Working... (esc to interrupt)",
|
||||
);
|
||||
this.statusContainer.addChild(this.loadingAnimation);
|
||||
this.ui.requestRender();
|
||||
break;
|
||||
|
|
@ -718,28 +730,9 @@ export class TuiRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
private getThinkingBorderColor(level: ThinkingLevel): (str: string) => string {
|
||||
// More thinking = more color (gray → dim colors → bright colors)
|
||||
switch (level) {
|
||||
case "off":
|
||||
return chalk.gray;
|
||||
case "minimal":
|
||||
return chalk.dim.blue;
|
||||
case "low":
|
||||
return chalk.blue;
|
||||
case "medium":
|
||||
return chalk.cyan;
|
||||
case "high":
|
||||
return chalk.magenta;
|
||||
default:
|
||||
return chalk.gray;
|
||||
}
|
||||
}
|
||||
|
||||
private updateEditorBorderColor(): void {
|
||||
const level = this.agent.state.thinkingLevel || "off";
|
||||
const color = this.getThinkingBorderColor(level);
|
||||
this.editor.borderColor = color;
|
||||
this.editor.borderColor = theme.getThinkingBorderColor(level);
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
|
|
@ -747,7 +740,7 @@ export class TuiRenderer {
|
|||
// Only cycle if model supports thinking
|
||||
if (!this.agent.state.model?.reasoning) {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.dim("Current model does not support thinking"), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("dim", "Current model does not support thinking"), 1, 0));
|
||||
this.ui.requestRender();
|
||||
return;
|
||||
}
|
||||
|
|
@ -769,7 +762,7 @@ export class TuiRenderer {
|
|||
|
||||
// Show brief notification
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.dim(`Thinking level: ${nextLevel}`), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("dim", `Thinking level: ${nextLevel}`), 1, 0));
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
|
|
@ -794,7 +787,7 @@ export class TuiRenderer {
|
|||
|
||||
if (modelsToUse.length === 1) {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.dim("Only one model in scope"), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("dim", "Only one model in scope"), 1, 0));
|
||||
this.ui.requestRender();
|
||||
return;
|
||||
}
|
||||
|
|
@ -824,7 +817,7 @@ export class TuiRenderer {
|
|||
|
||||
// Show notification
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.dim(`Switched to ${nextModel.name || nextModel.id}`), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("dim", `Switched to ${nextModel.name || nextModel.id}`), 1, 0));
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
|
|
@ -849,14 +842,14 @@ export class TuiRenderer {
|
|||
showError(errorMessage: string): void {
|
||||
// Show error message in the chat
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.red(`Error: ${errorMessage}`), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
showWarning(warningMessage: string): void {
|
||||
// Show warning message in the chat
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.yellow(`Warning: ${warningMessage}`), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
|
|
@ -876,7 +869,7 @@ export class TuiRenderer {
|
|||
|
||||
// Show confirmation message with proper spacing
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
const confirmText = new Text(chalk.dim(`Thinking level: ${level}`), 1, 0);
|
||||
const confirmText = new Text(theme.fg("dim", `Thinking level: ${level}`), 1, 0);
|
||||
this.chatContainer.addChild(confirmText);
|
||||
|
||||
// Hide selector and show editor again
|
||||
|
|
@ -918,7 +911,7 @@ export class TuiRenderer {
|
|||
|
||||
// Show confirmation message with proper spacing
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
const confirmText = new Text(chalk.dim(`Queue mode: ${mode}`), 1, 0);
|
||||
const confirmText = new Text(theme.fg("dim", `Queue mode: ${mode}`), 1, 0);
|
||||
this.chatContainer.addChild(confirmText);
|
||||
|
||||
// Hide selector and show editor again
|
||||
|
|
@ -956,15 +949,27 @@ export class TuiRenderer {
|
|||
currentTheme,
|
||||
(themeName) => {
|
||||
// Apply the selected theme
|
||||
setTheme(themeName);
|
||||
const result = setTheme(themeName);
|
||||
|
||||
// Save theme to settings
|
||||
this.settingsManager.setTheme(themeName);
|
||||
|
||||
// Show confirmation message with proper spacing
|
||||
// Invalidate all components to clear cached rendering
|
||||
this.ui.invalidate();
|
||||
|
||||
// Show confirmation or error message
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
const confirmText = new Text(chalk.dim(`Theme: ${themeName}`), 1, 0);
|
||||
this.chatContainer.addChild(confirmText);
|
||||
if (result.success) {
|
||||
const confirmText = new Text(theme.fg("dim", `Theme: ${themeName}`), 1, 0);
|
||||
this.chatContainer.addChild(confirmText);
|
||||
} else {
|
||||
const errorText = new Text(
|
||||
theme.fg("error", `Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`),
|
||||
1,
|
||||
0,
|
||||
);
|
||||
this.chatContainer.addChild(errorText);
|
||||
}
|
||||
|
||||
// Hide selector and show editor again
|
||||
this.hideThemeSelector();
|
||||
|
|
@ -975,6 +980,15 @@ export class TuiRenderer {
|
|||
this.hideThemeSelector();
|
||||
this.ui.requestRender();
|
||||
},
|
||||
(themeName) => {
|
||||
// Preview theme on selection change
|
||||
const result = setTheme(themeName);
|
||||
if (result.success) {
|
||||
this.ui.invalidate();
|
||||
this.ui.requestRender();
|
||||
}
|
||||
// If failed, theme already fell back to dark, just don't re-render
|
||||
},
|
||||
);
|
||||
|
||||
// Replace editor with selector
|
||||
|
|
@ -1007,7 +1021,7 @@ export class TuiRenderer {
|
|||
|
||||
// Show confirmation message with proper spacing
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
const confirmText = new Text(chalk.dim(`Model: ${model.id}`), 1, 0);
|
||||
const confirmText = new Text(theme.fg("dim", `Model: ${model.id}`), 1, 0);
|
||||
this.chatContainer.addChild(confirmText);
|
||||
|
||||
// Hide selector and show editor again
|
||||
|
|
@ -1055,7 +1069,7 @@ export class TuiRenderer {
|
|||
// Don't show selector if there are no messages or only one message
|
||||
if (userMessages.length <= 1) {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.dim("No messages to branch from"), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("dim", "No messages to branch from"), 1, 0));
|
||||
this.ui.requestRender();
|
||||
return;
|
||||
}
|
||||
|
|
@ -1088,7 +1102,7 @@ export class TuiRenderer {
|
|||
// Show confirmation message
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(
|
||||
new Text(chalk.dim(`Branched to new session from message ${messageIndex}`), 1, 0),
|
||||
new Text(theme.fg("dim", `Branched to new session from message ${messageIndex}`), 1, 0),
|
||||
);
|
||||
|
||||
// Put the selected message in the editor
|
||||
|
|
@ -1127,7 +1141,9 @@ export class TuiRenderer {
|
|||
const loggedInProviders = listOAuthProviders();
|
||||
if (loggedInProviders.length === 0) {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.dim("No OAuth providers logged in. Use /login first."), 1, 0));
|
||||
this.chatContainer.addChild(
|
||||
new Text(theme.fg("dim", "No OAuth providers logged in. Use /login first."), 1, 0),
|
||||
);
|
||||
this.ui.requestRender();
|
||||
return;
|
||||
}
|
||||
|
|
@ -1144,7 +1160,7 @@ export class TuiRenderer {
|
|||
if (mode === "login") {
|
||||
// Handle login
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.dim(`Logging in to ${providerId}...`), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("dim", `Logging in to ${providerId}...`), 1, 0));
|
||||
this.ui.requestRender();
|
||||
|
||||
try {
|
||||
|
|
@ -1153,11 +1169,11 @@ export class TuiRenderer {
|
|||
(url: string) => {
|
||||
// Show auth URL to user
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.cyan("Opening browser to:"), 1, 0));
|
||||
this.chatContainer.addChild(new Text(chalk.cyan(url), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("accent", "Opening browser to:"), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("accent", url), 1, 0));
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(
|
||||
new Text(chalk.yellow("Paste the authorization code below:"), 1, 0),
|
||||
new Text(theme.fg("warning", "Paste the authorization code below:"), 1, 0),
|
||||
);
|
||||
this.ui.requestRender();
|
||||
|
||||
|
|
@ -1189,8 +1205,12 @@ export class TuiRenderer {
|
|||
|
||||
// Success
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.green(`✓ Successfully logged in to ${providerId}`), 1, 0));
|
||||
this.chatContainer.addChild(new Text(chalk.dim(`Tokens saved to ~/.pi/agent/oauth.json`), 1, 0));
|
||||
this.chatContainer.addChild(
|
||||
new Text(theme.fg("success", `✓ Successfully logged in to ${providerId}`), 1, 0),
|
||||
);
|
||||
this.chatContainer.addChild(
|
||||
new Text(theme.fg("dim", `Tokens saved to ~/.pi/agent/oauth.json`), 1, 0),
|
||||
);
|
||||
this.ui.requestRender();
|
||||
} catch (error: any) {
|
||||
this.showError(`Login failed: ${error.message}`);
|
||||
|
|
@ -1202,10 +1222,10 @@ export class TuiRenderer {
|
|||
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(
|
||||
new Text(chalk.green(`✓ Successfully logged out of ${providerId}`), 1, 0),
|
||||
new Text(theme.fg("success", `✓ Successfully logged out of ${providerId}`), 1, 0),
|
||||
);
|
||||
this.chatContainer.addChild(
|
||||
new Text(chalk.dim(`Credentials removed from ~/.pi/agent/oauth.json`), 1, 0),
|
||||
new Text(theme.fg("dim", `Credentials removed from ~/.pi/agent/oauth.json`), 1, 0),
|
||||
);
|
||||
this.ui.requestRender();
|
||||
} catch (error: any) {
|
||||
|
|
@ -1246,13 +1266,13 @@ export class TuiRenderer {
|
|||
|
||||
// Show success message in chat - matching thinking level style
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(chalk.dim(`Session exported to: ${filePath}`), 1, 0));
|
||||
this.chatContainer.addChild(new Text(theme.fg("dim", `Session exported to: ${filePath}`), 1, 0));
|
||||
this.ui.requestRender();
|
||||
} catch (error: any) {
|
||||
// Show error message in chat
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(
|
||||
new Text(chalk.red(`Failed to export session: ${error.message || "Unknown error"}`), 1, 0),
|
||||
new Text(theme.fg("error", `Failed to export session: ${error.message || "Unknown error"}`), 1, 0),
|
||||
);
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
|
@ -1299,29 +1319,29 @@ export class TuiRenderer {
|
|||
const totalTokens = totalInput + totalOutput + totalCacheRead + totalCacheWrite;
|
||||
|
||||
// Build info text
|
||||
let info = `${chalk.bold("Session Info")}\n\n`;
|
||||
info += `${chalk.dim("File:")} ${sessionFile}\n`;
|
||||
info += `${chalk.dim("ID:")} ${this.sessionManager.getSessionId()}\n\n`;
|
||||
info += `${chalk.bold("Messages")}\n`;
|
||||
info += `${chalk.dim("User:")} ${userMessages}\n`;
|
||||
info += `${chalk.dim("Assistant:")} ${assistantMessages}\n`;
|
||||
info += `${chalk.dim("Tool Calls:")} ${toolCalls}\n`;
|
||||
info += `${chalk.dim("Tool Results:")} ${toolResults}\n`;
|
||||
info += `${chalk.dim("Total:")} ${totalMessages}\n\n`;
|
||||
info += `${chalk.bold("Tokens")}\n`;
|
||||
info += `${chalk.dim("Input:")} ${totalInput.toLocaleString()}\n`;
|
||||
info += `${chalk.dim("Output:")} ${totalOutput.toLocaleString()}\n`;
|
||||
let info = `${theme.bold("Session Info")}\n\n`;
|
||||
info += `${theme.fg("dim", "File:")} ${sessionFile}\n`;
|
||||
info += `${theme.fg("dim", "ID:")} ${this.sessionManager.getSessionId()}\n\n`;
|
||||
info += `${theme.bold("Messages")}\n`;
|
||||
info += `${theme.fg("dim", "User:")} ${userMessages}\n`;
|
||||
info += `${theme.fg("dim", "Assistant:")} ${assistantMessages}\n`;
|
||||
info += `${theme.fg("dim", "Tool Calls:")} ${toolCalls}\n`;
|
||||
info += `${theme.fg("dim", "Tool Results:")} ${toolResults}\n`;
|
||||
info += `${theme.fg("dim", "Total:")} ${totalMessages}\n\n`;
|
||||
info += `${theme.bold("Tokens")}\n`;
|
||||
info += `${theme.fg("dim", "Input:")} ${totalInput.toLocaleString()}\n`;
|
||||
info += `${theme.fg("dim", "Output:")} ${totalOutput.toLocaleString()}\n`;
|
||||
if (totalCacheRead > 0) {
|
||||
info += `${chalk.dim("Cache Read:")} ${totalCacheRead.toLocaleString()}\n`;
|
||||
info += `${theme.fg("dim", "Cache Read:")} ${totalCacheRead.toLocaleString()}\n`;
|
||||
}
|
||||
if (totalCacheWrite > 0) {
|
||||
info += `${chalk.dim("Cache Write:")} ${totalCacheWrite.toLocaleString()}\n`;
|
||||
info += `${theme.fg("dim", "Cache Write:")} ${totalCacheWrite.toLocaleString()}\n`;
|
||||
}
|
||||
info += `${chalk.dim("Total:")} ${totalTokens.toLocaleString()}\n`;
|
||||
info += `${theme.fg("dim", "Total:")} ${totalTokens.toLocaleString()}\n`;
|
||||
|
||||
if (totalCost > 0) {
|
||||
info += `\n${chalk.bold("Cost")}\n`;
|
||||
info += `${chalk.dim("Total:")} ${totalCost.toFixed(4)}`;
|
||||
info += `\n${theme.bold("Cost")}\n`;
|
||||
info += `${theme.fg("dim", "Total:")} ${totalCost.toFixed(4)}`;
|
||||
}
|
||||
|
||||
// Show info in chat
|
||||
|
|
@ -1345,11 +1365,11 @@ export class TuiRenderer {
|
|||
|
||||
// Display in chat
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new DynamicBorder(chalk.cyan));
|
||||
this.ui.addChild(new Text(chalk.bold.cyan("What's New"), 1, 0));
|
||||
this.chatContainer.addChild(new DynamicBorder());
|
||||
this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
|
||||
this.ui.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1));
|
||||
this.chatContainer.addChild(new DynamicBorder(chalk.cyan));
|
||||
this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, getMarkdownTheme()));
|
||||
this.chatContainer.addChild(new DynamicBorder());
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
|
|
@ -1360,7 +1380,7 @@ export class TuiRenderer {
|
|||
this.pendingMessagesContainer.addChild(new Spacer(1));
|
||||
|
||||
for (const message of this.queuedMessages) {
|
||||
const queuedText = chalk.dim("Queued: " + message);
|
||||
const queuedText = theme.fg("dim", "Queued: " + message);
|
||||
this.pendingMessagesContainer.addChild(new TruncatedText(queuedText, 1, 0));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,6 @@
|
|||
import { type Component, Container, Spacer, Text } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
|
||||
/**
|
||||
* Dynamic border component that adjusts to viewport width
|
||||
*/
|
||||
class DynamicBorder implements Component {
|
||||
private colorFn: (text: string) => string;
|
||||
|
||||
constructor(colorFn: (text: string) => string = chalk.blue) {
|
||||
this.colorFn = colorFn;
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
return [this.colorFn("─".repeat(Math.max(1, width)))];
|
||||
}
|
||||
}
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
||||
interface UserMessageItem {
|
||||
index: number; // Index in the full messages array
|
||||
|
|
@ -39,11 +25,15 @@ class UserMessageList implements Component {
|
|||
this.selectedIndex = Math.max(0, messages.length - 1);
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
// No cached state to invalidate currently
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
const lines: string[] = [];
|
||||
|
||||
if (this.messages.length === 0) {
|
||||
lines.push(chalk.gray(" No user messages found"));
|
||||
lines.push(theme.fg("muted", " No user messages found"));
|
||||
return lines;
|
||||
}
|
||||
|
||||
|
|
@ -63,24 +53,24 @@ class UserMessageList implements Component {
|
|||
const normalizedMessage = message.text.replace(/\n/g, " ").trim();
|
||||
|
||||
// First line: cursor + message
|
||||
const cursor = isSelected ? chalk.blue("› ") : " ";
|
||||
const cursor = isSelected ? theme.fg("accent", "› ") : " ";
|
||||
const maxMsgWidth = width - 2; // Account for cursor
|
||||
const truncatedMsg = normalizedMessage.substring(0, maxMsgWidth);
|
||||
const messageLine = cursor + (isSelected ? chalk.bold(truncatedMsg) : truncatedMsg);
|
||||
const messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg);
|
||||
|
||||
lines.push(messageLine);
|
||||
|
||||
// Second line: metadata (position in history)
|
||||
const position = i + 1;
|
||||
const metadata = ` Message ${position} of ${this.messages.length}`;
|
||||
const metadataLine = chalk.dim(metadata);
|
||||
const metadataLine = theme.fg("muted", metadata);
|
||||
lines.push(metadataLine);
|
||||
lines.push(""); // Blank line between messages
|
||||
}
|
||||
|
||||
// Add scroll indicator if needed
|
||||
if (startIndex > 0 || endIndex < this.messages.length) {
|
||||
const scrollInfo = chalk.gray(` (${this.selectedIndex + 1}/${this.messages.length})`);
|
||||
const scrollInfo = theme.fg("muted", ` (${this.selectedIndex + 1}/${this.messages.length})`);
|
||||
lines.push(scrollInfo);
|
||||
}
|
||||
|
||||
|
|
@ -129,8 +119,8 @@ export class UserMessageSelectorComponent extends Container {
|
|||
|
||||
// Add header
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(new Text(chalk.bold("Branch from Message"), 1, 0));
|
||||
this.addChild(new Text(chalk.dim("Select a message to create a new branch from that point"), 1, 0));
|
||||
this.addChild(new Text(theme.bold("Branch from Message"), 1, 0));
|
||||
this.addChild(new Text(theme.fg("muted", "Select a message to create a new branch from that point"), 1, 0));
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(new DynamicBorder());
|
||||
this.addChild(new Spacer(1));
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Container, Markdown, Spacer } from "@mariozechner/pi-tui";
|
||||
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
||||
|
||||
/**
|
||||
* Component that renders a user message
|
||||
|
|
@ -11,6 +12,11 @@ export class UserMessageComponent extends Container {
|
|||
if (!isFirst) {
|
||||
this.addChild(new Spacer(1));
|
||||
}
|
||||
this.addChild(new Markdown(text, 1, 1, { bgColor: "#343541" }));
|
||||
this.addChild(
|
||||
new Markdown(text, 1, 1, getMarkdownTheme(), {
|
||||
bgColor: (text: string) => theme.bg("userMessageBg", text),
|
||||
color: (text: string) => theme.fg("userMessageText", text),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
75
packages/coding-agent/test/test-theme-colors.ts
Normal file
75
packages/coding-agent/test/test-theme-colors.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { initTheme, theme } from "../src/theme/theme.js";
|
||||
|
||||
// Initialize with dark theme explicitly
|
||||
process.env.COLORTERM = "truecolor";
|
||||
initTheme("dark");
|
||||
|
||||
console.log("\n=== Foreground Colors ===\n");
|
||||
|
||||
// Core UI colors
|
||||
console.log("accent:", theme.fg("accent", "Sample text"));
|
||||
console.log("border:", theme.fg("border", "Sample text"));
|
||||
console.log("borderAccent:", theme.fg("borderAccent", "Sample text"));
|
||||
console.log("borderMuted:", theme.fg("borderMuted", "Sample text"));
|
||||
console.log("success:", theme.fg("success", "Sample text"));
|
||||
console.log("error:", theme.fg("error", "Sample text"));
|
||||
console.log("warning:", theme.fg("warning", "Sample text"));
|
||||
console.log("muted:", theme.fg("muted", "Sample text"));
|
||||
console.log("dim:", theme.fg("dim", "Sample text"));
|
||||
console.log("text:", theme.fg("text", "Sample text"));
|
||||
|
||||
console.log("\n=== Message Text Colors ===\n");
|
||||
console.log("userMessageText:", theme.fg("userMessageText", "Sample text"));
|
||||
console.log("toolTitle:", theme.fg("toolTitle", "Sample text"));
|
||||
console.log("toolOutput:", theme.fg("toolOutput", "Sample text"));
|
||||
|
||||
console.log("\n=== Markdown Colors ===\n");
|
||||
console.log("mdHeading:", theme.fg("mdHeading", "Sample text"));
|
||||
console.log("mdLink:", theme.fg("mdLink", "Sample text"));
|
||||
console.log("mdCode:", theme.fg("mdCode", "Sample text"));
|
||||
console.log("mdCodeBlock:", theme.fg("mdCodeBlock", "Sample text"));
|
||||
console.log("mdCodeBlockBorder:", theme.fg("mdCodeBlockBorder", "Sample text"));
|
||||
console.log("mdQuote:", theme.fg("mdQuote", "Sample text"));
|
||||
console.log("mdQuoteBorder:", theme.fg("mdQuoteBorder", "Sample text"));
|
||||
console.log("mdHr:", theme.fg("mdHr", "Sample text"));
|
||||
console.log("mdListBullet:", theme.fg("mdListBullet", "Sample text"));
|
||||
|
||||
console.log("\n=== Tool Diff Colors ===\n");
|
||||
console.log("toolDiffAdded:", theme.fg("toolDiffAdded", "Sample text"));
|
||||
console.log("toolDiffRemoved:", theme.fg("toolDiffRemoved", "Sample text"));
|
||||
console.log("toolDiffContext:", theme.fg("toolDiffContext", "Sample text"));
|
||||
|
||||
console.log("\n=== Thinking Border Colors ===\n");
|
||||
console.log("thinkingOff:", theme.fg("thinkingOff", "Sample text"));
|
||||
console.log("thinkingMinimal:", theme.fg("thinkingMinimal", "Sample text"));
|
||||
console.log("thinkingLow:", theme.fg("thinkingLow", "Sample text"));
|
||||
console.log("thinkingMedium:", theme.fg("thinkingMedium", "Sample text"));
|
||||
console.log("thinkingHigh:", theme.fg("thinkingHigh", "Sample text"));
|
||||
|
||||
console.log("\n=== Background Colors ===\n");
|
||||
console.log("userMessageBg:", theme.bg("userMessageBg", " Sample background text "));
|
||||
console.log("toolPendingBg:", theme.bg("toolPendingBg", " Sample background text "));
|
||||
console.log("toolSuccessBg:", theme.bg("toolSuccessBg", " Sample background text "));
|
||||
console.log("toolErrorBg:", theme.bg("toolErrorBg", " Sample background text "));
|
||||
|
||||
console.log("\n=== Raw ANSI Codes ===\n");
|
||||
console.log("thinkingMedium ANSI:", JSON.stringify(theme.getFgAnsi("thinkingMedium")));
|
||||
console.log("accent ANSI:", JSON.stringify(theme.getFgAnsi("accent")));
|
||||
console.log("muted ANSI:", JSON.stringify(theme.getFgAnsi("muted")));
|
||||
console.log("dim ANSI:", JSON.stringify(theme.getFgAnsi("dim")));
|
||||
|
||||
console.log("\n=== Direct RGB Test ===\n");
|
||||
console.log("Gray #6c6c6c: \x1b[38;2;108;108;108mSample text\x1b[0m");
|
||||
console.log("Gray #444444: \x1b[38;2;68;68;68mSample text\x1b[0m");
|
||||
console.log("Gray #303030: \x1b[38;2;48;48;48mSample text\x1b[0m");
|
||||
|
||||
console.log("\n=== Hex Color Test ===\n");
|
||||
console.log("Direct #00d7ff test: \x1b[38;2;0;215;255mBRIGHT CYAN\x1b[0m");
|
||||
console.log("Theme cyan (should match above):", theme.fg("accent", "BRIGHT CYAN"));
|
||||
|
||||
console.log("\n=== Environment ===\n");
|
||||
console.log("TERM:", process.env.TERM);
|
||||
console.log("COLORTERM:", process.env.COLORTERM);
|
||||
console.log("Color mode:", theme.getColorMode());
|
||||
|
||||
console.log("\n");
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@mariozechner/pi",
|
||||
"version": "0.7.29",
|
||||
"version": "0.8.0",
|
||||
"description": "CLI tool for managing vLLM deployments on GPU pods",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-agent": "^0.7.29",
|
||||
"@mariozechner/pi-agent": "^0.8.0",
|
||||
"chalk": "^5.5.0"
|
||||
},
|
||||
"devDependencies": {}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@mariozechner/pi-proxy",
|
||||
"version": "0.7.29",
|
||||
"version": "0.8.0",
|
||||
"type": "module",
|
||||
"description": "CORS and authentication proxy for pi-ai",
|
||||
"main": "dist/index.js",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@mariozechner/pi-tui",
|
||||
"version": "0.7.29",
|
||||
"version": "0.8.0",
|
||||
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import chalk from "chalk";
|
||||
import type { AutocompleteProvider, CombinedAutocompleteProvider } from "../autocomplete.js";
|
||||
import type { Component } from "../tui.js";
|
||||
import { SelectList } from "./select-list.js";
|
||||
import { SelectList, type SelectListTheme } from "./select-list.js";
|
||||
|
||||
interface EditorState {
|
||||
lines: string[];
|
||||
|
|
@ -15,8 +14,9 @@ interface LayoutLine {
|
|||
cursorPos?: number;
|
||||
}
|
||||
|
||||
export interface TextEditorConfig {
|
||||
// Configuration options for text editor (none currently)
|
||||
export interface EditorTheme {
|
||||
borderColor: (str: string) => string;
|
||||
selectList: SelectListTheme;
|
||||
}
|
||||
|
||||
export class Editor implements Component {
|
||||
|
|
@ -26,10 +26,10 @@ export class Editor implements Component {
|
|||
cursorCol: 0,
|
||||
};
|
||||
|
||||
private config: TextEditorConfig = {};
|
||||
private theme: EditorTheme;
|
||||
|
||||
// Border color (can be changed dynamically)
|
||||
public borderColor: (str: string) => string = chalk.gray;
|
||||
public borderColor: (str: string) => string;
|
||||
|
||||
// Autocomplete support
|
||||
private autocompleteProvider?: AutocompleteProvider;
|
||||
|
|
@ -49,20 +49,19 @@ export class Editor implements Component {
|
|||
public onChange?: (text: string) => void;
|
||||
public disableSubmit: boolean = false;
|
||||
|
||||
constructor(config?: TextEditorConfig) {
|
||||
if (config) {
|
||||
this.config = { ...this.config, ...config };
|
||||
}
|
||||
}
|
||||
|
||||
configure(config: Partial<TextEditorConfig>): void {
|
||||
this.config = { ...this.config, ...config };
|
||||
constructor(theme: EditorTheme) {
|
||||
this.theme = theme;
|
||||
this.borderColor = theme.borderColor;
|
||||
}
|
||||
|
||||
setAutocompleteProvider(provider: AutocompleteProvider): void {
|
||||
this.autocompleteProvider = provider;
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
// No cached state to invalidate currently
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
const horizontal = this.borderColor("─");
|
||||
|
||||
|
|
@ -806,7 +805,7 @@ export class Editor implements Component {
|
|||
|
||||
if (suggestions && suggestions.items.length > 0) {
|
||||
this.autocompletePrefix = suggestions.prefix;
|
||||
this.autocompleteList = new SelectList(suggestions.items, 5);
|
||||
this.autocompleteList = new SelectList(suggestions.items, 5, this.theme.selectList);
|
||||
this.isAutocompleting = true;
|
||||
} else {
|
||||
this.cancelAutocomplete();
|
||||
|
|
@ -851,7 +850,7 @@ export class Editor implements Component {
|
|||
|
||||
if (suggestions && suggestions.items.length > 0) {
|
||||
this.autocompletePrefix = suggestions.prefix;
|
||||
this.autocompleteList = new SelectList(suggestions.items, 5);
|
||||
this.autocompleteList = new SelectList(suggestions.items, 5, this.theme.selectList);
|
||||
this.isAutocompleting = true;
|
||||
} else {
|
||||
this.cancelAutocomplete();
|
||||
|
|
@ -881,7 +880,7 @@ export class Editor implements Component {
|
|||
this.autocompletePrefix = suggestions.prefix;
|
||||
if (this.autocompleteList) {
|
||||
// Update the existing list with new items
|
||||
this.autocompleteList = new SelectList(suggestions.items, 5);
|
||||
this.autocompleteList = new SelectList(suggestions.items, 5, this.theme.selectList);
|
||||
}
|
||||
} else {
|
||||
// No more matches, cancel autocomplete
|
||||
|
|
|
|||
|
|
@ -129,6 +129,10 @@ export class Input implements Component {
|
|||
this.cursor += cleanText.length;
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
// No cached state to invalidate currently
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
// Calculate visible window
|
||||
const prompt = "> ";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import chalk from "chalk";
|
||||
import type { TUI } from "../tui.js";
|
||||
import { Text } from "./text.js";
|
||||
|
||||
|
|
@ -13,6 +12,8 @@ export class Loader extends Text {
|
|||
|
||||
constructor(
|
||||
ui: TUI,
|
||||
private spinnerColorFn: (str: string) => string,
|
||||
private messageColorFn: (str: string) => string,
|
||||
private message: string = "Loading...",
|
||||
) {
|
||||
super("", 1, 0);
|
||||
|
|
@ -46,7 +47,7 @@ export class Loader extends Text {
|
|||
|
||||
private updateDisplay() {
|
||||
const frame = this.frames[this.currentFrame];
|
||||
this.setText(`${chalk.cyan(frame)} ${chalk.dim(this.message)}`);
|
||||
this.setText(`${this.spinnerColorFn(frame)} ${this.messageColorFn(this.message)}`);
|
||||
if (this.ui) {
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,16 @@
|
|||
import { Chalk } from "chalk";
|
||||
import { marked, type Token } from "marked";
|
||||
import type { Component } from "../tui.js";
|
||||
import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.js";
|
||||
|
||||
// Use a chalk instance with color level 3 for consistent ANSI output
|
||||
const colorChalk = new Chalk({ level: 3 });
|
||||
|
||||
/**
|
||||
* Default text styling for markdown content.
|
||||
* Applied to all text unless overridden by markdown formatting.
|
||||
*/
|
||||
export interface DefaultTextStyle {
|
||||
/** Foreground color - named color or hex string like "#ff0000" */
|
||||
color?: string;
|
||||
/** Background color - named color or hex string like "#ff0000" */
|
||||
bgColor?: string;
|
||||
/** Foreground color function */
|
||||
color?: (text: string) => string;
|
||||
/** Background color function */
|
||||
bgColor?: (text: string) => string;
|
||||
/** Bold text */
|
||||
bold?: boolean;
|
||||
/** Italic text */
|
||||
|
|
@ -32,6 +28,7 @@ export interface DefaultTextStyle {
|
|||
export interface MarkdownTheme {
|
||||
heading: (text: string) => string;
|
||||
link: (text: string) => string;
|
||||
linkUrl: (text: string) => string;
|
||||
code: (text: string) => string;
|
||||
codeBlock: (text: string) => string;
|
||||
codeBlockBorder: (text: string) => string;
|
||||
|
|
@ -39,6 +36,10 @@ export interface MarkdownTheme {
|
|||
quoteBorder: (text: string) => string;
|
||||
hr: (text: string) => string;
|
||||
listBullet: (text: string) => string;
|
||||
bold: (text: string) => string;
|
||||
italic: (text: string) => string;
|
||||
strikethrough: (text: string) => string;
|
||||
underline: (text: string) => string;
|
||||
}
|
||||
|
||||
export class Markdown implements Component {
|
||||
|
|
@ -46,7 +47,7 @@ export class Markdown implements Component {
|
|||
private paddingX: number; // Left/right padding
|
||||
private paddingY: number; // Top/bottom padding
|
||||
private defaultTextStyle?: DefaultTextStyle;
|
||||
private theme?: MarkdownTheme;
|
||||
private theme: MarkdownTheme;
|
||||
|
||||
// Cache for rendered output
|
||||
private cachedText?: string;
|
||||
|
|
@ -54,22 +55,25 @@ export class Markdown implements Component {
|
|||
private cachedLines?: string[];
|
||||
|
||||
constructor(
|
||||
text: string = "",
|
||||
paddingX: number = 1,
|
||||
paddingY: number = 1,
|
||||
text: string,
|
||||
paddingX: number,
|
||||
paddingY: number,
|
||||
theme: MarkdownTheme,
|
||||
defaultTextStyle?: DefaultTextStyle,
|
||||
theme?: MarkdownTheme,
|
||||
) {
|
||||
this.text = text;
|
||||
this.paddingX = paddingX;
|
||||
this.paddingY = paddingY;
|
||||
this.defaultTextStyle = defaultTextStyle;
|
||||
this.theme = theme;
|
||||
this.defaultTextStyle = defaultTextStyle;
|
||||
}
|
||||
|
||||
setText(text: string): void {
|
||||
this.text = text;
|
||||
// Invalidate cache when text changes
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
this.cachedText = undefined;
|
||||
this.cachedWidth = undefined;
|
||||
this.cachedLines = undefined;
|
||||
|
|
@ -119,14 +123,14 @@ export class Markdown implements Component {
|
|||
// Add margins and background to each wrapped line
|
||||
const leftMargin = " ".repeat(this.paddingX);
|
||||
const rightMargin = " ".repeat(this.paddingX);
|
||||
const bgRgb = this.defaultTextStyle?.bgColor ? this.parseBgColor() : undefined;
|
||||
const bgFn = this.defaultTextStyle?.bgColor;
|
||||
const contentLines: string[] = [];
|
||||
|
||||
for (const line of wrappedLines) {
|
||||
const lineWithMargins = leftMargin + line + rightMargin;
|
||||
|
||||
if (bgRgb) {
|
||||
contentLines.push(applyBackgroundToLine(lineWithMargins, width, bgRgb));
|
||||
if (bgFn) {
|
||||
contentLines.push(applyBackgroundToLine(lineWithMargins, width, bgFn));
|
||||
} else {
|
||||
// No background - just pad to width
|
||||
const visibleLen = visibleWidth(lineWithMargins);
|
||||
|
|
@ -139,7 +143,7 @@ export class Markdown implements Component {
|
|||
const emptyLine = " ".repeat(width);
|
||||
const emptyLines: string[] = [];
|
||||
for (let i = 0; i < this.paddingY; i++) {
|
||||
const line = bgRgb ? applyBackgroundToLine(emptyLine, width, bgRgb) : emptyLine;
|
||||
const line = bgFn ? applyBackgroundToLine(emptyLine, width, bgFn) : emptyLine;
|
||||
emptyLines.push(line);
|
||||
}
|
||||
|
||||
|
|
@ -154,39 +158,6 @@ export class Markdown implements Component {
|
|||
return result.length > 0 ? result : [""];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse background color from defaultTextStyle to RGB values
|
||||
*/
|
||||
private parseBgColor(): { r: number; g: number; b: number } | undefined {
|
||||
if (!this.defaultTextStyle?.bgColor) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (this.defaultTextStyle.bgColor.startsWith("#")) {
|
||||
// Hex color
|
||||
const hex = this.defaultTextStyle.bgColor.substring(1);
|
||||
return {
|
||||
r: Number.parseInt(hex.substring(0, 2), 16),
|
||||
g: Number.parseInt(hex.substring(2, 4), 16),
|
||||
b: Number.parseInt(hex.substring(4, 6), 16),
|
||||
};
|
||||
}
|
||||
|
||||
// Named colors - map to RGB (common terminal colors)
|
||||
const colorMap: Record<string, { r: number; g: number; b: number }> = {
|
||||
bgBlack: { r: 0, g: 0, b: 0 },
|
||||
bgRed: { r: 255, g: 0, b: 0 },
|
||||
bgGreen: { r: 0, g: 255, b: 0 },
|
||||
bgYellow: { r: 255, g: 255, b: 0 },
|
||||
bgBlue: { r: 0, g: 0, b: 255 },
|
||||
bgMagenta: { r: 255, g: 0, b: 255 },
|
||||
bgCyan: { r: 0, g: 255, b: 255 },
|
||||
bgWhite: { r: 255, g: 255, b: 255 },
|
||||
};
|
||||
|
||||
return colorMap[this.defaultTextStyle.bgColor];
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply default text style to a string.
|
||||
* This is the base styling applied to all text content.
|
||||
|
|
@ -202,31 +173,21 @@ export class Markdown implements Component {
|
|||
|
||||
// Apply foreground color (NOT background - that's applied at padding stage)
|
||||
if (this.defaultTextStyle.color) {
|
||||
if (this.defaultTextStyle.color.startsWith("#")) {
|
||||
// Hex color
|
||||
const hex = this.defaultTextStyle.color.substring(1);
|
||||
const r = Number.parseInt(hex.substring(0, 2), 16);
|
||||
const g = Number.parseInt(hex.substring(2, 4), 16);
|
||||
const b = Number.parseInt(hex.substring(4, 6), 16);
|
||||
styled = colorChalk.rgb(r, g, b)(styled);
|
||||
} else {
|
||||
// Named color
|
||||
styled = (colorChalk as any)[this.defaultTextStyle.color](styled);
|
||||
}
|
||||
styled = this.defaultTextStyle.color(styled);
|
||||
}
|
||||
|
||||
// Apply text decorations
|
||||
// Apply text decorations using this.theme
|
||||
if (this.defaultTextStyle.bold) {
|
||||
styled = colorChalk.bold(styled);
|
||||
styled = this.theme.bold(styled);
|
||||
}
|
||||
if (this.defaultTextStyle.italic) {
|
||||
styled = colorChalk.italic(styled);
|
||||
styled = this.theme.italic(styled);
|
||||
}
|
||||
if (this.defaultTextStyle.strikethrough) {
|
||||
styled = colorChalk.strikethrough(styled);
|
||||
styled = this.theme.strikethrough(styled);
|
||||
}
|
||||
if (this.defaultTextStyle.underline) {
|
||||
styled = colorChalk.underline(styled);
|
||||
styled = this.theme.underline(styled);
|
||||
}
|
||||
|
||||
return styled;
|
||||
|
|
@ -240,13 +201,15 @@ export class Markdown implements Component {
|
|||
const headingLevel = token.depth;
|
||||
const headingPrefix = "#".repeat(headingLevel) + " ";
|
||||
const headingText = this.renderInlineTokens(token.tokens || []);
|
||||
let styledHeading: string;
|
||||
if (headingLevel === 1) {
|
||||
lines.push(colorChalk.bold.underline.yellow(headingText));
|
||||
styledHeading = this.theme.heading(this.theme.bold(this.theme.underline(headingText)));
|
||||
} else if (headingLevel === 2) {
|
||||
lines.push(colorChalk.bold.yellow(headingText));
|
||||
styledHeading = this.theme.heading(this.theme.bold(headingText));
|
||||
} else {
|
||||
lines.push(colorChalk.bold(headingPrefix + headingText));
|
||||
styledHeading = this.theme.heading(this.theme.bold(headingPrefix + headingText));
|
||||
}
|
||||
lines.push(styledHeading);
|
||||
lines.push(""); // Add spacing after headings
|
||||
break;
|
||||
}
|
||||
|
|
@ -262,13 +225,13 @@ export class Markdown implements Component {
|
|||
}
|
||||
|
||||
case "code": {
|
||||
lines.push(colorChalk.gray("```" + (token.lang || "")));
|
||||
lines.push(this.theme.codeBlockBorder("```" + (token.lang || "")));
|
||||
// Split code by newlines and style each line
|
||||
const codeLines = token.text.split("\n");
|
||||
for (const codeLine of codeLines) {
|
||||
lines.push(colorChalk.dim(" ") + colorChalk.green(codeLine));
|
||||
lines.push(" " + this.theme.codeBlock(codeLine));
|
||||
}
|
||||
lines.push(colorChalk.gray("```"));
|
||||
lines.push(this.theme.codeBlockBorder("```"));
|
||||
lines.push(""); // Add spacing after code blocks
|
||||
break;
|
||||
}
|
||||
|
|
@ -291,14 +254,14 @@ export class Markdown implements Component {
|
|||
const quoteText = this.renderInlineTokens(token.tokens || []);
|
||||
const quoteLines = quoteText.split("\n");
|
||||
for (const quoteLine of quoteLines) {
|
||||
lines.push(colorChalk.gray("│ ") + colorChalk.italic(quoteLine));
|
||||
lines.push(this.theme.quoteBorder("│ ") + this.theme.quote(this.theme.italic(quoteLine)));
|
||||
}
|
||||
lines.push(""); // Add spacing after blockquotes
|
||||
break;
|
||||
}
|
||||
|
||||
case "hr":
|
||||
lines.push(colorChalk.gray("─".repeat(Math.min(width, 80))));
|
||||
lines.push(this.theme.hr("─".repeat(Math.min(width, 80))));
|
||||
lines.push(""); // Add spacing after horizontal rules
|
||||
break;
|
||||
|
||||
|
|
@ -339,31 +302,31 @@ export class Markdown implements Component {
|
|||
case "strong": {
|
||||
// Apply bold, then reapply default style after
|
||||
const boldContent = this.renderInlineTokens(token.tokens || []);
|
||||
result += colorChalk.bold(boldContent) + this.applyDefaultStyle("");
|
||||
result += this.theme.bold(boldContent) + this.applyDefaultStyle("");
|
||||
break;
|
||||
}
|
||||
|
||||
case "em": {
|
||||
// Apply italic, then reapply default style after
|
||||
const italicContent = this.renderInlineTokens(token.tokens || []);
|
||||
result += colorChalk.italic(italicContent) + this.applyDefaultStyle("");
|
||||
result += this.theme.italic(italicContent) + this.applyDefaultStyle("");
|
||||
break;
|
||||
}
|
||||
|
||||
case "codespan":
|
||||
// Apply code styling without backticks
|
||||
result += colorChalk.cyan(token.text) + this.applyDefaultStyle("");
|
||||
result += this.theme.code(token.text) + this.applyDefaultStyle("");
|
||||
break;
|
||||
|
||||
case "link": {
|
||||
const linkText = this.renderInlineTokens(token.tokens || []);
|
||||
// If link text matches href, only show the link once
|
||||
if (linkText === token.href) {
|
||||
result += colorChalk.underline.blue(linkText) + this.applyDefaultStyle("");
|
||||
result += this.theme.link(this.theme.underline(linkText)) + this.applyDefaultStyle("");
|
||||
} else {
|
||||
result +=
|
||||
colorChalk.underline.blue(linkText) +
|
||||
colorChalk.gray(` (${token.href})`) +
|
||||
this.theme.link(this.theme.underline(linkText)) +
|
||||
this.theme.linkUrl(` (${token.href})`) +
|
||||
this.applyDefaultStyle("");
|
||||
}
|
||||
break;
|
||||
|
|
@ -375,7 +338,7 @@ export class Markdown implements Component {
|
|||
|
||||
case "del": {
|
||||
const delContent = this.renderInlineTokens(token.tokens || []);
|
||||
result += colorChalk.strikethrough(delContent) + this.applyDefaultStyle("");
|
||||
result += this.theme.strikethrough(delContent) + this.applyDefaultStyle("");
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -415,7 +378,7 @@ export class Markdown implements Component {
|
|||
lines.push(firstLine);
|
||||
} else {
|
||||
// Regular text content - add indent and bullet
|
||||
lines.push(indent + colorChalk.cyan(bullet) + firstLine);
|
||||
lines.push(indent + this.theme.listBullet(bullet) + firstLine);
|
||||
}
|
||||
|
||||
// Rest of the lines
|
||||
|
|
@ -432,7 +395,7 @@ export class Markdown implements Component {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
lines.push(indent + colorChalk.cyan(bullet));
|
||||
lines.push(indent + this.theme.listBullet(bullet));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -463,12 +426,12 @@ export class Markdown implements Component {
|
|||
lines.push(text);
|
||||
} else if (token.type === "code") {
|
||||
// Code block in list item
|
||||
lines.push(colorChalk.gray("```" + (token.lang || "")));
|
||||
lines.push(this.theme.codeBlockBorder("```" + (token.lang || "")));
|
||||
const codeLines = token.text.split("\n");
|
||||
for (const codeLine of codeLines) {
|
||||
lines.push(colorChalk.dim(" ") + colorChalk.green(codeLine));
|
||||
lines.push(" " + this.theme.codeBlock(codeLine));
|
||||
}
|
||||
lines.push(colorChalk.gray("```"));
|
||||
lines.push(this.theme.codeBlockBorder("```"));
|
||||
} else {
|
||||
// Other token types - try to render as inline
|
||||
const text = this.renderInlineTokens([token]);
|
||||
|
|
@ -515,7 +478,7 @@ export class Markdown implements Component {
|
|||
// Render header
|
||||
const headerCells = token.header.map((cell, i) => {
|
||||
const text = this.renderInlineTokens(cell.tokens || []);
|
||||
return colorChalk.bold(text.padEnd(columnWidths[i]));
|
||||
return this.theme.bold(text.padEnd(columnWidths[i]));
|
||||
});
|
||||
lines.push("│ " + headerCells.join(" │ ") + " │");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import chalk from "chalk";
|
||||
import type { Component } from "../tui.js";
|
||||
|
||||
export interface SelectItem {
|
||||
|
|
@ -7,19 +6,30 @@ export interface SelectItem {
|
|||
description?: string;
|
||||
}
|
||||
|
||||
export interface SelectListTheme {
|
||||
selectedPrefix: (text: string) => string;
|
||||
selectedText: (text: string) => string;
|
||||
description: (text: string) => string;
|
||||
scrollInfo: (text: string) => string;
|
||||
noMatch: (text: string) => string;
|
||||
}
|
||||
|
||||
export class SelectList implements Component {
|
||||
private items: SelectItem[] = [];
|
||||
private filteredItems: SelectItem[] = [];
|
||||
private selectedIndex: number = 0;
|
||||
private maxVisible: number = 5;
|
||||
private theme: SelectListTheme;
|
||||
|
||||
public onSelect?: (item: SelectItem) => void;
|
||||
public onCancel?: () => void;
|
||||
public onSelectionChange?: (item: SelectItem) => void;
|
||||
|
||||
constructor(items: SelectItem[], maxVisible: number = 5) {
|
||||
constructor(items: SelectItem[], maxVisible: number, theme: SelectListTheme) {
|
||||
this.items = items;
|
||||
this.filteredItems = items;
|
||||
this.maxVisible = maxVisible;
|
||||
this.theme = theme;
|
||||
}
|
||||
|
||||
setFilter(filter: string): void {
|
||||
|
|
@ -32,12 +42,16 @@ export class SelectList implements Component {
|
|||
this.selectedIndex = Math.max(0, Math.min(index, this.filteredItems.length - 1));
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
// No cached state to invalidate currently
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
const lines: string[] = [];
|
||||
|
||||
// If no items match filter, show message
|
||||
if (this.filteredItems.length === 0) {
|
||||
lines.push(chalk.gray(" No matching commands"));
|
||||
lines.push(this.theme.noMatch(" No matching commands"));
|
||||
return lines;
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +72,7 @@ export class SelectList implements Component {
|
|||
let line = "";
|
||||
if (isSelected) {
|
||||
// Use arrow indicator for selection
|
||||
const prefix = chalk.blue("→ ");
|
||||
const prefix = this.theme.selectedPrefix("→ ");
|
||||
const prefixWidth = 2; // "→ " is 2 characters visually
|
||||
const displayValue = item.label || item.value;
|
||||
|
||||
|
|
@ -74,16 +88,20 @@ export class SelectList implements Component {
|
|||
|
||||
if (remainingWidth > 10) {
|
||||
const truncatedDesc = item.description.substring(0, remainingWidth);
|
||||
line = prefix + chalk.blue(truncatedValue) + chalk.gray(spacing + truncatedDesc);
|
||||
const selectedText = this.theme.selectedText(truncatedValue);
|
||||
const descText = this.theme.description(spacing + truncatedDesc);
|
||||
line = prefix + selectedText + descText;
|
||||
} else {
|
||||
// Not enough space for description
|
||||
const maxWidth = width - prefixWidth - 2;
|
||||
line = prefix + chalk.blue(displayValue.substring(0, maxWidth));
|
||||
const selectedText = this.theme.selectedText(displayValue.substring(0, maxWidth));
|
||||
line = prefix + selectedText;
|
||||
}
|
||||
} else {
|
||||
// No description or not enough width
|
||||
const maxWidth = width - prefixWidth - 2;
|
||||
line = prefix + chalk.blue(displayValue.substring(0, maxWidth));
|
||||
const selectedText = this.theme.selectedText(displayValue.substring(0, maxWidth));
|
||||
line = prefix + selectedText;
|
||||
}
|
||||
} else {
|
||||
const displayValue = item.label || item.value;
|
||||
|
|
@ -101,7 +119,8 @@ export class SelectList implements Component {
|
|||
|
||||
if (remainingWidth > 10) {
|
||||
const truncatedDesc = item.description.substring(0, remainingWidth);
|
||||
line = prefix + truncatedValue + chalk.gray(spacing + truncatedDesc);
|
||||
const descText = this.theme.description(spacing + truncatedDesc);
|
||||
line = prefix + truncatedValue + descText;
|
||||
} else {
|
||||
// Not enough space for description
|
||||
const maxWidth = width - prefix.length - 2;
|
||||
|
|
@ -123,8 +142,7 @@ export class SelectList implements Component {
|
|||
// Truncate if too long for terminal
|
||||
const maxWidth = width - 2;
|
||||
const truncated = scrollText.substring(0, maxWidth);
|
||||
const scrollInfo = chalk.gray(truncated);
|
||||
lines.push(scrollInfo);
|
||||
lines.push(this.theme.scrollInfo(truncated));
|
||||
}
|
||||
|
||||
return lines;
|
||||
|
|
@ -134,10 +152,12 @@ export class SelectList implements Component {
|
|||
// Up arrow
|
||||
if (keyData === "\x1b[A") {
|
||||
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
||||
this.notifySelectionChange();
|
||||
}
|
||||
// Down arrow
|
||||
else if (keyData === "\x1b[B") {
|
||||
this.selectedIndex = Math.min(this.filteredItems.length - 1, this.selectedIndex + 1);
|
||||
this.notifySelectionChange();
|
||||
}
|
||||
// Enter
|
||||
else if (keyData === "\r") {
|
||||
|
|
@ -154,6 +174,13 @@ export class SelectList implements Component {
|
|||
}
|
||||
}
|
||||
|
||||
private notifySelectionChange(): void {
|
||||
const selectedItem = this.filteredItems[this.selectedIndex];
|
||||
if (selectedItem && this.onSelectionChange) {
|
||||
this.onSelectionChange(selectedItem);
|
||||
}
|
||||
}
|
||||
|
||||
getSelectedItem(): SelectItem | null {
|
||||
const item = this.filteredItems[this.selectedIndex];
|
||||
return item || null;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ export class Spacer implements Component {
|
|||
this.lines = lines;
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
// No cached state to invalidate currently
|
||||
}
|
||||
|
||||
render(_width: number): string[] {
|
||||
const result: string[] = [];
|
||||
for (let i = 0; i < this.lines; i++) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import { Chalk } from "chalk";
|
||||
import type { Component } from "../tui.js";
|
||||
import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.js";
|
||||
|
||||
const colorChalk = new Chalk({ level: 3 });
|
||||
|
||||
/**
|
||||
* Text component - displays multi-line text with word wrapping
|
||||
*/
|
||||
|
|
@ -11,23 +8,18 @@ export class Text implements Component {
|
|||
private text: string;
|
||||
private paddingX: number; // Left/right padding
|
||||
private paddingY: number; // Top/bottom padding
|
||||
private customBgRgb?: { r: number; g: number; b: number };
|
||||
private customBgFn?: (text: string) => string;
|
||||
|
||||
// Cache for rendered output
|
||||
private cachedText?: string;
|
||||
private cachedWidth?: number;
|
||||
private cachedLines?: string[];
|
||||
|
||||
constructor(
|
||||
text: string = "",
|
||||
paddingX: number = 1,
|
||||
paddingY: number = 1,
|
||||
customBgRgb?: { r: number; g: number; b: number },
|
||||
) {
|
||||
constructor(text: string = "", paddingX: number = 1, paddingY: number = 1, customBgFn?: (text: string) => string) {
|
||||
this.text = text;
|
||||
this.paddingX = paddingX;
|
||||
this.paddingY = paddingY;
|
||||
this.customBgRgb = customBgRgb;
|
||||
this.customBgFn = customBgFn;
|
||||
}
|
||||
|
||||
setText(text: string): void {
|
||||
|
|
@ -37,8 +29,14 @@ export class Text implements Component {
|
|||
this.cachedLines = undefined;
|
||||
}
|
||||
|
||||
setCustomBgRgb(customBgRgb?: { r: number; g: number; b: number }): void {
|
||||
this.customBgRgb = customBgRgb;
|
||||
setCustomBgFn(customBgFn?: (text: string) => string): void {
|
||||
this.customBgFn = customBgFn;
|
||||
this.cachedText = undefined;
|
||||
this.cachedWidth = undefined;
|
||||
this.cachedLines = undefined;
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
this.cachedText = undefined;
|
||||
this.cachedWidth = undefined;
|
||||
this.cachedLines = undefined;
|
||||
|
|
@ -78,8 +76,8 @@ export class Text implements Component {
|
|||
const lineWithMargins = leftMargin + line + rightMargin;
|
||||
|
||||
// Apply background if specified (this also pads to full width)
|
||||
if (this.customBgRgb) {
|
||||
contentLines.push(applyBackgroundToLine(lineWithMargins, width, this.customBgRgb));
|
||||
if (this.customBgFn) {
|
||||
contentLines.push(applyBackgroundToLine(lineWithMargins, width, this.customBgFn));
|
||||
} else {
|
||||
// No background - just pad to width with spaces
|
||||
const visibleLen = visibleWidth(lineWithMargins);
|
||||
|
|
@ -92,7 +90,7 @@ export class Text implements Component {
|
|||
const emptyLine = " ".repeat(width);
|
||||
const emptyLines: string[] = [];
|
||||
for (let i = 0; i < this.paddingY; i++) {
|
||||
const line = this.customBgRgb ? applyBackgroundToLine(emptyLine, width, this.customBgRgb) : emptyLine;
|
||||
const line = this.customBgFn ? applyBackgroundToLine(emptyLine, width, this.customBgFn) : emptyLine;
|
||||
emptyLines.push(line);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,20 +15,34 @@ export class TruncatedText implements Component {
|
|||
this.paddingY = paddingY;
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
// No cached state to invalidate currently
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
const result: string[] = [];
|
||||
|
||||
// Empty line padded to width
|
||||
const emptyLine = " ".repeat(width);
|
||||
|
||||
// Add vertical padding above
|
||||
for (let i = 0; i < this.paddingY; i++) {
|
||||
result.push("");
|
||||
result.push(emptyLine);
|
||||
}
|
||||
|
||||
// Calculate available width after horizontal padding
|
||||
const availableWidth = Math.max(1, width - this.paddingX * 2);
|
||||
|
||||
// Take only the first line (stop at newline)
|
||||
let singleLineText = this.text;
|
||||
const newlineIndex = this.text.indexOf("\n");
|
||||
if (newlineIndex !== -1) {
|
||||
singleLineText = this.text.substring(0, newlineIndex);
|
||||
}
|
||||
|
||||
// Truncate text if needed (accounting for ANSI codes)
|
||||
let displayText = this.text;
|
||||
const textVisibleWidth = visibleWidth(this.text);
|
||||
let displayText = singleLineText;
|
||||
const textVisibleWidth = visibleWidth(singleLineText);
|
||||
|
||||
if (textVisibleWidth > availableWidth) {
|
||||
// Need to truncate - walk through the string character by character
|
||||
|
|
@ -38,18 +52,21 @@ export class TruncatedText implements Component {
|
|||
const ellipsisWidth = 3;
|
||||
const targetWidth = availableWidth - ellipsisWidth;
|
||||
|
||||
while (i < this.text.length && currentWidth < targetWidth) {
|
||||
// Skip ANSI escape sequences
|
||||
if (this.text[i] === "\x1b" && this.text[i + 1] === "[") {
|
||||
while (i < singleLineText.length && currentWidth < targetWidth) {
|
||||
// Skip ANSI escape sequences (include them in output but don't count width)
|
||||
if (singleLineText[i] === "\x1b" && singleLineText[i + 1] === "[") {
|
||||
let j = i + 2;
|
||||
while (j < this.text.length && !/[a-zA-Z]/.test(this.text[j])) {
|
||||
while (j < singleLineText.length && !/[a-zA-Z]/.test(singleLineText[j])) {
|
||||
j++;
|
||||
}
|
||||
i = j + 1;
|
||||
// Include the final letter of the escape sequence
|
||||
j++;
|
||||
truncateAt = j;
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
const char = this.text[i];
|
||||
const char = singleLineText[i];
|
||||
const charWidth = visibleWidth(char);
|
||||
|
||||
if (currentWidth + charWidth > targetWidth) {
|
||||
|
|
@ -61,16 +78,25 @@ export class TruncatedText implements Component {
|
|||
i++;
|
||||
}
|
||||
|
||||
displayText = this.text.substring(0, truncateAt) + "...";
|
||||
// Add reset code before ellipsis to prevent styling leaking into it
|
||||
displayText = singleLineText.substring(0, truncateAt) + "\x1b[0m...";
|
||||
}
|
||||
|
||||
// Add horizontal padding
|
||||
const paddingStr = " ".repeat(this.paddingX);
|
||||
result.push(paddingStr + displayText);
|
||||
const leftPadding = " ".repeat(this.paddingX);
|
||||
const rightPadding = " ".repeat(this.paddingX);
|
||||
const lineWithPadding = leftPadding + displayText + rightPadding;
|
||||
|
||||
// Pad line to exactly width characters
|
||||
const lineVisibleWidth = visibleWidth(lineWithPadding);
|
||||
const paddingNeeded = Math.max(0, width - lineVisibleWidth);
|
||||
const finalLine = lineWithPadding + " ".repeat(paddingNeeded);
|
||||
|
||||
result.push(finalLine);
|
||||
|
||||
// Add vertical padding below
|
||||
for (let i = 0; i < this.paddingY; i++) {
|
||||
result.push("");
|
||||
result.push(emptyLine);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ export {
|
|||
type SlashCommand,
|
||||
} from "./autocomplete.js";
|
||||
// Components
|
||||
export { Editor, type TextEditorConfig } from "./components/editor.js";
|
||||
export { Editor, type EditorTheme } from "./components/editor.js";
|
||||
export { Input } from "./components/input.js";
|
||||
export { Loader } from "./components/loader.js";
|
||||
export { type DefaultTextStyle, Markdown, type MarkdownTheme } from "./components/markdown.js";
|
||||
export { type SelectItem, SelectList } from "./components/select-list.js";
|
||||
export { type SelectItem, SelectList, type SelectListTheme } from "./components/select-list.js";
|
||||
export { Spacer } from "./components/spacer.js";
|
||||
export { Text } from "./components/text.js";
|
||||
export { TruncatedText } from "./components/truncated-text.js";
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@ export interface Component {
|
|||
* Optional handler for keyboard input when component has focus
|
||||
*/
|
||||
handleInput?(data: string): void;
|
||||
|
||||
/**
|
||||
* Invalidate any cached rendering state.
|
||||
* Called when theme changes or when component needs to re-render from scratch.
|
||||
*/
|
||||
invalidate(): void;
|
||||
}
|
||||
|
||||
export { visibleWidth };
|
||||
|
|
@ -45,6 +51,12 @@ export class Container implements Component {
|
|||
this.children = [];
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
for (const child of this.children) {
|
||||
child.invalidate?.();
|
||||
}
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
const lines: string[] = [];
|
||||
for (const child of this.children) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import { Chalk } from "chalk";
|
||||
import stringWidth from "string-width";
|
||||
|
||||
const colorChalk = new Chalk({ level: 3 });
|
||||
|
||||
/**
|
||||
* Calculate the visible width of a string in terminal columns.
|
||||
*/
|
||||
|
|
@ -245,30 +242,18 @@ function breakLongWord(word: string, width: number, tracker: AnsiCodeTracker): s
|
|||
/**
|
||||
* Apply background color to a line, padding to full width.
|
||||
*
|
||||
* Handles the tricky case where content contains \x1b[0m resets that would
|
||||
* kill the background color. We reapply the background after any reset.
|
||||
*
|
||||
* @param line - Line of text (may contain ANSI codes)
|
||||
* @param width - Total width to pad to
|
||||
* @param bgRgb - Background RGB color
|
||||
* @param bgFn - Background color function
|
||||
* @returns Line with background applied and padded to width
|
||||
*/
|
||||
export function applyBackgroundToLine(line: string, width: number, bgRgb: { r: number; g: number; b: number }): string {
|
||||
const bgStart = `\x1b[48;2;${bgRgb.r};${bgRgb.g};${bgRgb.b}m`;
|
||||
const bgEnd = "\x1b[49m";
|
||||
|
||||
export function applyBackgroundToLine(line: string, width: number, bgFn: (text: string) => string): string {
|
||||
// Calculate padding needed
|
||||
const visibleLen = visibleWidth(line);
|
||||
const paddingNeeded = Math.max(0, width - visibleLen);
|
||||
const padding = " ".repeat(paddingNeeded);
|
||||
|
||||
// Strategy: wrap content + padding in background, then fix any 0m resets
|
||||
// Apply background to content + padding
|
||||
const withPadding = line + padding;
|
||||
const withBg = bgStart + withPadding + bgEnd;
|
||||
|
||||
// Find all \x1b[0m or \x1b[49m that would kill background
|
||||
// Replace with reset + background reapplication
|
||||
const fixedBg = withBg.replace(/\x1b\[0m/g, `\x1b[0m${bgStart}`);
|
||||
|
||||
return fixedBg;
|
||||
return bgFn(withPadding);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
* Simple chat interface demo using tui.ts
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import { CombinedAutocompleteProvider } from "../src/autocomplete.js";
|
||||
import { Editor } from "../src/components/editor.js";
|
||||
import { Loader } from "../src/components/loader.js";
|
||||
|
|
@ -9,6 +10,7 @@ import { Markdown } from "../src/components/markdown.js";
|
|||
import { Text } from "../src/components/text.js";
|
||||
import { ProcessTerminal } from "../src/terminal.js";
|
||||
import { TUI } from "../src/tui.js";
|
||||
import { defaultEditorTheme, defaultMarkdownTheme } from "./test-themes.js";
|
||||
|
||||
// Create terminal
|
||||
const terminal = new ProcessTerminal();
|
||||
|
|
@ -22,7 +24,7 @@ tui.addChild(
|
|||
);
|
||||
|
||||
// Create editor with autocomplete
|
||||
const editor = new Editor();
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
// Set up autocomplete provider with slash commands and file completion
|
||||
const autocompleteProvider = new CombinedAutocompleteProvider(
|
||||
|
|
@ -78,12 +80,17 @@ editor.onSubmit = (value: string) => {
|
|||
isResponding = true;
|
||||
editor.disableSubmit = true;
|
||||
|
||||
const userMessage = new Markdown(value, 1, 1, { bgColor: "#343541" });
|
||||
const userMessage = new Markdown(value, 1, 1, defaultMarkdownTheme);
|
||||
|
||||
const children = tui.children;
|
||||
children.splice(children.length - 1, 0, userMessage);
|
||||
|
||||
const loader = new Loader(tui, "Thinking...");
|
||||
const loader = new Loader(
|
||||
tui,
|
||||
(s) => chalk.cyan(s),
|
||||
(s) => chalk.dim(s),
|
||||
"Thinking...",
|
||||
);
|
||||
children.splice(children.length - 1, 0, loader);
|
||||
|
||||
tui.requestRender();
|
||||
|
|
@ -105,7 +112,7 @@ editor.onSubmit = (value: string) => {
|
|||
const randomResponse = responses[Math.floor(Math.random() * responses.length)];
|
||||
|
||||
// Add assistant message with no background (transparent)
|
||||
const botMessage = new Markdown(randomResponse);
|
||||
const botMessage = new Markdown(randomResponse, 1, 1, defaultMarkdownTheme);
|
||||
children.splice(children.length - 1, 0, botMessage);
|
||||
|
||||
// Re-enable submit
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import assert from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
import { Editor } from "../src/components/editor.js";
|
||||
import { defaultEditorTheme } from "./test-themes.js";
|
||||
|
||||
describe("Editor component", () => {
|
||||
describe("Unicode text editing behavior", () => {
|
||||
it("inserts mixed ASCII, umlauts, and emojis as literal text", () => {
|
||||
const editor = new Editor();
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
editor.handleInput("H");
|
||||
editor.handleInput("e");
|
||||
|
|
@ -24,7 +25,7 @@ describe("Editor component", () => {
|
|||
});
|
||||
|
||||
it("deletes single-code-unit unicode characters (umlauts) with Backspace", () => {
|
||||
const editor = new Editor();
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
editor.handleInput("ä");
|
||||
editor.handleInput("ö");
|
||||
|
|
@ -38,7 +39,7 @@ describe("Editor component", () => {
|
|||
});
|
||||
|
||||
it("deletes multi-code-unit emojis with repeated Backspace", () => {
|
||||
const editor = new Editor();
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
editor.handleInput("😀");
|
||||
editor.handleInput("👍");
|
||||
|
|
@ -52,7 +53,7 @@ describe("Editor component", () => {
|
|||
});
|
||||
|
||||
it("inserts characters at the correct position after cursor movement over umlauts", () => {
|
||||
const editor = new Editor();
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
editor.handleInput("ä");
|
||||
editor.handleInput("ö");
|
||||
|
|
@ -70,7 +71,7 @@ describe("Editor component", () => {
|
|||
});
|
||||
|
||||
it("moves cursor in code units across multi-code-unit emojis before insertion", () => {
|
||||
const editor = new Editor();
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
editor.handleInput("😀");
|
||||
editor.handleInput("👍");
|
||||
|
|
@ -92,7 +93,7 @@ describe("Editor component", () => {
|
|||
});
|
||||
|
||||
it("preserves umlauts across line breaks", () => {
|
||||
const editor = new Editor();
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
editor.handleInput("ä");
|
||||
editor.handleInput("ö");
|
||||
|
|
@ -107,7 +108,7 @@ describe("Editor component", () => {
|
|||
});
|
||||
|
||||
it("replaces the entire document with unicode text via setText (paste simulation)", () => {
|
||||
const editor = new Editor();
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
// Simulate bracketed paste / programmatic replacement
|
||||
editor.setText("Hällö Wörld! 😀 äöüÄÖÜß");
|
||||
|
|
@ -117,7 +118,7 @@ describe("Editor component", () => {
|
|||
});
|
||||
|
||||
it("moves cursor to document start on Ctrl+A and inserts at the beginning", () => {
|
||||
const editor = new Editor();
|
||||
const editor = new Editor(defaultEditorTheme);
|
||||
|
||||
editor.handleInput("a");
|
||||
editor.handleInput("b");
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ class KeyLogger implements Component {
|
|||
this.tui.requestRender();
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
// No cached state to invalidate currently
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
const lines: string[] = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import assert from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
import chalk from "chalk";
|
||||
import { Markdown } from "../src/components/markdown.js";
|
||||
import { defaultMarkdownTheme } from "./test-themes.js";
|
||||
|
||||
describe("Markdown component", () => {
|
||||
describe("Nested lists", () => {
|
||||
|
|
@ -12,6 +14,7 @@ describe("Markdown component", () => {
|
|||
- Item 2`,
|
||||
0,
|
||||
0,
|
||||
defaultMarkdownTheme,
|
||||
);
|
||||
|
||||
const lines = markdown.render(80);
|
||||
|
|
@ -37,6 +40,7 @@ describe("Markdown component", () => {
|
|||
- Level 4`,
|
||||
0,
|
||||
0,
|
||||
defaultMarkdownTheme,
|
||||
);
|
||||
|
||||
const lines = markdown.render(80);
|
||||
|
|
@ -57,6 +61,7 @@ describe("Markdown component", () => {
|
|||
2. Second`,
|
||||
0,
|
||||
0,
|
||||
defaultMarkdownTheme,
|
||||
);
|
||||
|
||||
const lines = markdown.render(80);
|
||||
|
|
@ -77,6 +82,7 @@ describe("Markdown component", () => {
|
|||
- More nested`,
|
||||
0,
|
||||
0,
|
||||
defaultMarkdownTheme,
|
||||
);
|
||||
|
||||
const lines = markdown.render(80);
|
||||
|
|
@ -97,6 +103,7 @@ describe("Markdown component", () => {
|
|||
| Bob | 25 |`,
|
||||
0,
|
||||
0,
|
||||
defaultMarkdownTheme,
|
||||
);
|
||||
|
||||
const lines = markdown.render(80);
|
||||
|
|
@ -120,6 +127,7 @@ describe("Markdown component", () => {
|
|||
| Long text | Middle | End |`,
|
||||
0,
|
||||
0,
|
||||
defaultMarkdownTheme,
|
||||
);
|
||||
|
||||
const lines = markdown.render(80);
|
||||
|
|
@ -141,6 +149,7 @@ describe("Markdown component", () => {
|
|||
| B | Short |`,
|
||||
0,
|
||||
0,
|
||||
defaultMarkdownTheme,
|
||||
);
|
||||
|
||||
const lines = markdown.render(80);
|
||||
|
|
@ -168,6 +177,7 @@ describe("Markdown component", () => {
|
|||
| A | B |`,
|
||||
0,
|
||||
0,
|
||||
defaultMarkdownTheme,
|
||||
);
|
||||
|
||||
const lines = markdown.render(80);
|
||||
|
|
@ -187,10 +197,16 @@ describe("Markdown component", () => {
|
|||
describe("Pre-styled text (thinking traces)", () => {
|
||||
it("should preserve gray italic styling after inline code", () => {
|
||||
// This replicates how thinking content is rendered in assistant-message.ts
|
||||
const markdown = new Markdown("This is thinking with `inline code` and more text after", 1, 0, {
|
||||
color: "gray",
|
||||
italic: true,
|
||||
});
|
||||
const markdown = new Markdown(
|
||||
"This is thinking with `inline code` and more text after",
|
||||
1,
|
||||
0,
|
||||
defaultMarkdownTheme,
|
||||
{
|
||||
color: (text) => chalk.gray(text),
|
||||
italic: true,
|
||||
},
|
||||
);
|
||||
|
||||
const lines = markdown.render(80);
|
||||
const joinedOutput = lines.join("\n");
|
||||
|
|
@ -208,10 +224,16 @@ describe("Markdown component", () => {
|
|||
});
|
||||
|
||||
it("should preserve gray italic styling after bold text", () => {
|
||||
const markdown = new Markdown("This is thinking with **bold text** and more after", 1, 0, {
|
||||
color: "gray",
|
||||
italic: true,
|
||||
});
|
||||
const markdown = new Markdown(
|
||||
"This is thinking with **bold text** and more after",
|
||||
1,
|
||||
0,
|
||||
defaultMarkdownTheme,
|
||||
{
|
||||
color: (text) => chalk.gray(text),
|
||||
italic: true,
|
||||
},
|
||||
);
|
||||
|
||||
const lines = markdown.render(80);
|
||||
const joinedOutput = lines.join("\n");
|
||||
|
|
@ -236,6 +258,7 @@ describe("Markdown component", () => {
|
|||
"This is text with <thinking>hidden content</thinking> that should be visible",
|
||||
0,
|
||||
0,
|
||||
defaultMarkdownTheme,
|
||||
);
|
||||
|
||||
const lines = markdown.render(80);
|
||||
|
|
@ -250,7 +273,7 @@ describe("Markdown component", () => {
|
|||
});
|
||||
|
||||
it("should render HTML tags in code blocks correctly", () => {
|
||||
const markdown = new Markdown("```html\n<div>Some HTML</div>\n```", 0, 0);
|
||||
const markdown = new Markdown("```html\n<div>Some HTML</div>\n```", 0, 0, defaultMarkdownTheme);
|
||||
|
||||
const lines = markdown.render(80);
|
||||
const plainLines = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, ""));
|
||||
|
|
|
|||
36
packages/tui/test/test-themes.ts
Normal file
36
packages/tui/test/test-themes.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* Default themes for TUI tests using chalk
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import type { EditorTheme, MarkdownTheme, SelectListTheme } from "../src/index.js";
|
||||
|
||||
export const defaultSelectListTheme: SelectListTheme = {
|
||||
selectedPrefix: (text: string) => chalk.blue(text),
|
||||
selectedText: (text: string) => chalk.bold(text),
|
||||
description: (text: string) => chalk.dim(text),
|
||||
scrollInfo: (text: string) => chalk.dim(text),
|
||||
noMatch: (text: string) => chalk.dim(text),
|
||||
};
|
||||
|
||||
export const defaultMarkdownTheme: MarkdownTheme = {
|
||||
heading: (text: string) => chalk.bold.cyan(text),
|
||||
link: (text: string) => chalk.blue(text),
|
||||
linkUrl: (text: string) => chalk.dim(text),
|
||||
code: (text: string) => chalk.yellow(text),
|
||||
codeBlock: (text: string) => chalk.green(text),
|
||||
codeBlockBorder: (text: string) => chalk.dim(text),
|
||||
quote: (text: string) => chalk.italic(text),
|
||||
quoteBorder: (text: string) => chalk.dim(text),
|
||||
hr: (text: string) => chalk.dim(text),
|
||||
listBullet: (text: string) => chalk.cyan(text),
|
||||
bold: (text: string) => chalk.bold(text),
|
||||
italic: (text: string) => chalk.italic(text),
|
||||
strikethrough: (text: string) => chalk.strikethrough(text),
|
||||
underline: (text: string) => chalk.underline(text),
|
||||
};
|
||||
|
||||
export const defaultEditorTheme: EditorTheme = {
|
||||
borderColor: (text: string) => chalk.dim(text),
|
||||
selectList: defaultSelectListTheme,
|
||||
};
|
||||
126
packages/tui/test/truncated-text.test.ts
Normal file
126
packages/tui/test/truncated-text.test.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import assert from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
import chalk from "chalk";
|
||||
import { TruncatedText } from "../src/components/truncated-text.js";
|
||||
import { visibleWidth } from "../src/utils.js";
|
||||
|
||||
describe("TruncatedText component", () => {
|
||||
it("pads output lines to exactly match width", () => {
|
||||
const text = new TruncatedText("Hello world", 1, 0);
|
||||
const lines = text.render(50);
|
||||
|
||||
// Should have exactly one content line (no vertical padding)
|
||||
assert.strictEqual(lines.length, 1);
|
||||
|
||||
// Line should be exactly 50 visible characters
|
||||
const visibleLen = visibleWidth(lines[0]);
|
||||
assert.strictEqual(visibleLen, 50);
|
||||
});
|
||||
|
||||
it("pads output with vertical padding lines to width", () => {
|
||||
const text = new TruncatedText("Hello", 0, 2);
|
||||
const lines = text.render(40);
|
||||
|
||||
// Should have 2 padding lines + 1 content line + 2 padding lines = 5 total
|
||||
assert.strictEqual(lines.length, 5);
|
||||
|
||||
// All lines should be exactly 40 characters
|
||||
for (const line of lines) {
|
||||
assert.strictEqual(visibleWidth(line), 40);
|
||||
}
|
||||
});
|
||||
|
||||
it("truncates long text and pads to width", () => {
|
||||
const longText = "This is a very long piece of text that will definitely exceed the available width";
|
||||
const text = new TruncatedText(longText, 1, 0);
|
||||
const lines = text.render(30);
|
||||
|
||||
assert.strictEqual(lines.length, 1);
|
||||
|
||||
// Should be exactly 30 characters
|
||||
assert.strictEqual(visibleWidth(lines[0]), 30);
|
||||
|
||||
// Should contain ellipsis
|
||||
const stripped = lines[0].replace(/\x1b\[[0-9;]*m/g, "");
|
||||
assert.ok(stripped.includes("..."));
|
||||
});
|
||||
|
||||
it("preserves ANSI codes in output and pads correctly", () => {
|
||||
const styledText = chalk.red("Hello") + " " + chalk.blue("world");
|
||||
const text = new TruncatedText(styledText, 1, 0);
|
||||
const lines = text.render(40);
|
||||
|
||||
assert.strictEqual(lines.length, 1);
|
||||
|
||||
// Should be exactly 40 visible characters (ANSI codes don't count)
|
||||
assert.strictEqual(visibleWidth(lines[0]), 40);
|
||||
|
||||
// Should preserve the color codes
|
||||
assert.ok(lines[0].includes("\x1b["));
|
||||
});
|
||||
|
||||
it("truncates styled text and adds reset code before ellipsis", () => {
|
||||
const longStyledText = chalk.red("This is a very long red text that will be truncated");
|
||||
const text = new TruncatedText(longStyledText, 1, 0);
|
||||
const lines = text.render(20);
|
||||
|
||||
assert.strictEqual(lines.length, 1);
|
||||
|
||||
// Should be exactly 20 visible characters
|
||||
assert.strictEqual(visibleWidth(lines[0]), 20);
|
||||
|
||||
// Should contain reset code before ellipsis
|
||||
assert.ok(lines[0].includes("\x1b[0m..."));
|
||||
});
|
||||
|
||||
it("handles text that fits exactly", () => {
|
||||
// With paddingX=1, available width is 30-2=28
|
||||
// "Hello world" is 11 chars, fits comfortably
|
||||
const text = new TruncatedText("Hello world", 1, 0);
|
||||
const lines = text.render(30);
|
||||
|
||||
assert.strictEqual(lines.length, 1);
|
||||
assert.strictEqual(visibleWidth(lines[0]), 30);
|
||||
|
||||
// Should NOT contain ellipsis
|
||||
const stripped = lines[0].replace(/\x1b\[[0-9;]*m/g, "");
|
||||
assert.ok(!stripped.includes("..."));
|
||||
});
|
||||
|
||||
it("handles empty text", () => {
|
||||
const text = new TruncatedText("", 1, 0);
|
||||
const lines = text.render(30);
|
||||
|
||||
assert.strictEqual(lines.length, 1);
|
||||
assert.strictEqual(visibleWidth(lines[0]), 30);
|
||||
});
|
||||
|
||||
it("stops at newline and only shows first line", () => {
|
||||
const multilineText = "First line\nSecond line\nThird line";
|
||||
const text = new TruncatedText(multilineText, 1, 0);
|
||||
const lines = text.render(40);
|
||||
|
||||
assert.strictEqual(lines.length, 1);
|
||||
assert.strictEqual(visibleWidth(lines[0]), 40);
|
||||
|
||||
// Should only contain "First line"
|
||||
const stripped = lines[0].replace(/\x1b\[[0-9;]*m/g, "").trim();
|
||||
assert.ok(stripped.includes("First line"));
|
||||
assert.ok(!stripped.includes("Second line"));
|
||||
assert.ok(!stripped.includes("Third line"));
|
||||
});
|
||||
|
||||
it("truncates first line even with newlines in text", () => {
|
||||
const longMultilineText = "This is a very long first line that needs truncation\nSecond line";
|
||||
const text = new TruncatedText(longMultilineText, 1, 0);
|
||||
const lines = text.render(25);
|
||||
|
||||
assert.strictEqual(lines.length, 1);
|
||||
assert.strictEqual(visibleWidth(lines[0]), 25);
|
||||
|
||||
// Should contain ellipsis and not second line
|
||||
const stripped = lines[0].replace(/\x1b\[[0-9;]*m/g, "");
|
||||
assert.ok(stripped.includes("..."));
|
||||
assert.ok(!stripped.includes("Second line"));
|
||||
});
|
||||
});
|
||||
|
|
@ -65,22 +65,24 @@ describe("wrapTextWithAnsi", () => {
|
|||
});
|
||||
|
||||
describe("applyBackgroundToLine", () => {
|
||||
const greenBg = (text: string) => chalk.bgGreen(text);
|
||||
|
||||
it("applies background to plain text and pads to width", () => {
|
||||
const line = "hello";
|
||||
const result = applyBackgroundToLine(line, 20, { r: 0, g: 255, b: 0 });
|
||||
const result = applyBackgroundToLine(line, 20, greenBg);
|
||||
|
||||
// Should be exactly 20 visible chars
|
||||
const stripped = result.replace(/\x1b\[[0-9;]*m/g, "");
|
||||
assert.strictEqual(stripped.length, 20);
|
||||
|
||||
// Should have background codes
|
||||
assert.ok(result.includes("\x1b[48;2;0;255;0m"));
|
||||
assert.ok(result.includes("\x1b[48") || result.includes("\x1b[42m"));
|
||||
assert.ok(result.includes("\x1b[49m"));
|
||||
});
|
||||
|
||||
it("handles text with ANSI codes and resets", () => {
|
||||
const line = chalk.bold("hello") + " world";
|
||||
const result = applyBackgroundToLine(line, 20, { r: 0, g: 255, b: 0 });
|
||||
const result = applyBackgroundToLine(line, 20, greenBg);
|
||||
|
||||
// Should be exactly 20 visible chars
|
||||
const stripped = result.replace(/\x1b\[[0-9;]*m/g, "");
|
||||
|
|
@ -90,13 +92,13 @@ describe("applyBackgroundToLine", () => {
|
|||
assert.ok(result.includes("\x1b[1m"));
|
||||
|
||||
// Should have background throughout (even after resets)
|
||||
assert.ok(result.includes("\x1b[48;2;0;255;0m"));
|
||||
assert.ok(result.includes("\x1b[48") || result.includes("\x1b[42m"));
|
||||
});
|
||||
|
||||
it("handles text with 0m resets by reapplying background", () => {
|
||||
// Simulate: bold text + reset + normal text
|
||||
const line = "\x1b[1mhello\x1b[0m world";
|
||||
const result = applyBackgroundToLine(line, 20, { r: 0, g: 255, b: 0 });
|
||||
const result = applyBackgroundToLine(line, 20, greenBg);
|
||||
|
||||
// Should NOT have black cells (spaces without background)
|
||||
// Pattern we DON'T want: 49m or 0m followed by spaces before bg reapplied
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@mariozechner/pi-web-ui",
|
||||
"version": "0.7.29",
|
||||
"version": "0.8.0",
|
||||
"description": "Reusable web UI components for AI chat interfaces powered by @mariozechner/pi-ai",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
|
@ -18,8 +18,8 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@lmstudio/sdk": "^1.5.0",
|
||||
"@mariozechner/pi-ai": "^0.7.29",
|
||||
"@mariozechner/pi-tui": "^0.7.29",
|
||||
"@mariozechner/pi-ai": "^0.8.0",
|
||||
"@mariozechner/pi-tui": "^0.8.0",
|
||||
"docx-preview": "^0.3.7",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide": "^0.544.0",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue