From d6db94a39d37fda632d53147592af8f53aa50e4b Mon Sep 17 00:00:00 2001 From: Erik Kotsiuba Date: Mon, 22 Jun 2026 16:41:37 +0200 Subject: [PATCH] fix: refresh pi dependency compatibility Update Pi packages to 0.79.9 and align pi-ask with current TUI/RPC behavior. - guard custom ask/settings flows to TUI mode with a clear non-TUI fallback - reset config cache on extension reload so edited settings are picked up - clean up remote ask flow listeners on session shutdown - forward editor focus/invalidation and keep @ file autocomplete explicit - document the non-TUI contract and cover the compatibility fixes with tests --- docs/contract.md | 6 +- package.json | 14 +- pnpm-lock.yaml | 235 ++++++++++++++++------------- src/answer-commands.ts | 8 +- src/ask-settings-command.ts | 2 +- src/ask-tool-helpers.ts | 4 +- src/ask-tool.ts | 2 +- src/config/store.ts | 4 + src/index.ts | 5 + src/remote-ask.ts | 8 +- src/ui/autocomplete.ts | 11 +- src/ui/controller.ts | 16 +- src/ui/show-settings.ts | 8 +- tests/answer-commands.test.ts | 47 ++++++ tests/ask-settings-command.test.ts | 38 +++++ tests/ask-tool.test.ts | 25 ++- tests/autocomplete.test.ts | 9 ++ tests/config-store.test.ts | 38 ++++- tests/input.test.ts | 74 +++++++++ tests/remote-ask.test.ts | 40 +++++ 20 files changed, 457 insertions(+), 137 deletions(-) create mode 100644 tests/answer-commands.test.ts create mode 100644 tests/autocomplete.test.ts diff --git a/docs/contract.md b/docs/contract.md index b338e7f..919179f 100644 --- a/docs/contract.md +++ b/docs/contract.md @@ -255,9 +255,9 @@ Dirty dismiss: - when `Confirm dismiss when dirty` is enabled, cancelling or dismissing a dirty ask flow requires the same action a second time - the dirty-dismiss warning stays visible until the user changes tabs in the ask flow -## Non-interactive mode +## Non-TUI and non-interactive modes -If `ctx.hasUI === false`, the tool returns a `Needs user input` message in `content` and a cancelled result in `details`. +The rich ask flow uses `ctx.ui.custom()` and opens only in TUI mode. In print, JSON, RPC, or any other non-TUI mode, the tool returns a `Needs user input: ask_user requires interactive TUI mode.` message in `content` and a cancelled result in `details` instead of opening custom UI. Validation is handled inside the tool so malformed calls produce the same structured error shape as other invalid payloads instead of relying on pre-execution schema failures. @@ -269,7 +269,7 @@ When enabled, pi-ask emits one best-effort external notification per ask session ## Remote inter-extension events -pi-ask exposes a local `pi.events` contract for trusted Pi extensions. It does not expose a network API and does not automate terminal keystrokes. +pi-ask exposes a local `pi.events` contract for trusted Pi extensions. It does not expose a network API and does not automate terminal keystrokes. RPC or headless integrations should use a trusted in-process bridge extension that consumes these events rather than expecting the TUI-only custom surface to open. Channels: diff --git a/package.json b/package.json index bf7f650..e2ef574 100644 --- a/package.json +++ b/package.json @@ -93,9 +93,9 @@ "@biomejs/biome": "2.4.12", "@commitlint/cli": "^19.8.1", "@commitlint/config-conventional": "^19.8.1", - "@earendil-works/pi-ai": "0.75.4", - "@earendil-works/pi-coding-agent": "0.75.4", - "@earendil-works/pi-tui": "0.75.4", + "@earendil-works/pi-ai": "0.79.9", + "@earendil-works/pi-coding-agent": "0.79.9", + "@earendil-works/pi-tui": "0.79.9", "@semantic-release/changelog": "^6.0.3", "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/git": "^10.0.1", @@ -118,10 +118,10 @@ }, "pnpm": { "overrides": { - "@earendil-works/pi-agent-core": "0.75.4", - "@earendil-works/pi-ai": "0.75.4", - "@earendil-works/pi-tui": "0.75.4", - "@earendil-works/pi-coding-agent": "0.75.4" + "@earendil-works/pi-agent-core": "0.79.9", + "@earendil-works/pi-ai": "0.79.9", + "@earendil-works/pi-tui": "0.79.9", + "@earendil-works/pi-coding-agent": "0.79.9" }, "ignoredBuiltDependencies": [ "lefthook" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5a40df..0990c5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,10 +5,10 @@ settings: excludeLinksFromLockfile: false overrides: - '@earendil-works/pi-agent-core': 0.75.4 - '@earendil-works/pi-ai': 0.75.4 - '@earendil-works/pi-tui': 0.75.4 - '@earendil-works/pi-coding-agent': 0.75.4 + '@earendil-works/pi-agent-core': 0.79.9 + '@earendil-works/pi-ai': 0.79.9 + '@earendil-works/pi-tui': 0.79.9 + '@earendil-works/pi-coding-agent': 0.79.9 importers: @@ -24,14 +24,14 @@ importers: specifier: ^19.8.1 version: 19.8.1 '@earendil-works/pi-ai': - specifier: 0.75.4 - version: 0.75.4(ws@8.20.0)(zod@4.3.6) + specifier: 0.79.9 + version: 0.79.9(ws@8.20.0)(zod@4.3.6) '@earendil-works/pi-coding-agent': - specifier: 0.75.4 - version: 0.75.4(ws@8.20.0)(zod@4.3.6) + specifier: 0.79.9 + version: 0.79.9(ws@8.20.0)(zod@4.3.6) '@earendil-works/pi-tui': - specifier: 0.75.4 - version: 0.75.4 + specifier: 0.79.9 + version: 0.79.9 '@semantic-release/changelog': specifier: ^6.0.3 version: 6.0.3(semantic-release@25.0.3(typescript@6.0.2)) @@ -367,22 +367,22 @@ packages: resolution: {integrity: sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==} engines: {node: '>=v18'} - '@earendil-works/pi-agent-core@0.75.4': - resolution: {integrity: sha512-cGYbysb4EqUf0B28OeqFq2ppm1XF3bYBOP71q9dv38yf/UJfzMjiXBeNelrcio+QWIoVrW+xzYm7sMzYIUc9Og==} + '@earendil-works/pi-agent-core@0.79.9': + resolution: {integrity: sha512-GsFbPR85nhncKoU9++fTKa11PzwUkAmkrKXo97dBOzi10Td72rVV6vmfxKjwPLHTzRbJMa0byr12YhODZ59yLA==} engines: {node: '>=22.19.0'} - '@earendil-works/pi-ai@0.75.4': - resolution: {integrity: sha512-m/w8Hh3vQ0rAycwJiJWdzkypkn4295f4eq/966lDRy8aX5sk6bgYXH8TQmL16TO7Uwc7MbJG0QoyFHgX8RqXUQ==} + '@earendil-works/pi-ai@0.79.9': + resolution: {integrity: sha512-fHmgNMONwCCE7bQAKbcz76sgm3iQuA7km1mpIc4H5xXd9+zhPh/faULz6ARkgjQE0EufHnfZPJY39+lNf8Sa9g==} engines: {node: '>=22.19.0'} hasBin: true - '@earendil-works/pi-coding-agent@0.75.4': - resolution: {integrity: sha512-Fb+FRo08b5H9pYKbQJ708/5OKL0+K/yclhfCMEhrBzSPTZZ4c85nY1YsBo4qwL20ohBMlBezHMRuHzcJ1ylEoQ==} + '@earendil-works/pi-coding-agent@0.79.9': + resolution: {integrity: sha512-8TZ796Zn0NE4vmhxG9hv4ZtJDGJzhqMjlmFg8ZkUKxfqB7LJa4ums2jSJKtnyAZfAamN6VzqzN0A82RNDqv8Ag==} engines: {node: '>=22.19.0'} hasBin: true - '@earendil-works/pi-tui@0.75.4': - resolution: {integrity: sha512-PDhKU7u6fmEcvHUFHzrRwGc/Ytokj/hO+X4RPf+MWKEGpvg3B1vHv88Ee+Dy33004tYkQF5YeXV4btJZcp5x1g==} + '@earendil-works/pi-tui@0.79.9': + resolution: {integrity: sha512-XcqfoGyoX64OSMQklMR1vG2MRi1TCPSUERRCmPDvYKCK6LtZoBqJ6idNCLRklNYveCj4gHDOzOsLhGqn04jmYw==} engines: {node: '>=22.19.0'} '@google/genai@1.52.0': @@ -394,76 +394,81 @@ packages: '@modelcontextprotocol/sdk': optional: true - '@mariozechner/clipboard-darwin-arm64@0.3.6': - resolution: {integrity: sha512-HjaisYCAbHi/1+N1yDAQHc8ZXGffufIUT5NSOSVR3f3AuMDusxTtnbK8tZ7JFDkShua1oNGZoNwQHsc8MPtE0Q==} + '@mariozechner/clipboard-darwin-arm64@0.3.9': + resolution: {integrity: sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@mariozechner/clipboard-darwin-universal@0.3.6': - resolution: {integrity: sha512-8BWtPjOtJOJoykml3w0fx0zRrfWP31mXrJwfoA7xzNprkZw1uolCNfgmjDiVBseoKjp16EGITz7bN+61qn8dWA==} + '@mariozechner/clipboard-darwin-universal@0.3.9': + resolution: {integrity: sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==} engines: {node: '>= 10'} os: [darwin] - '@mariozechner/clipboard-darwin-x64@0.3.6': - resolution: {integrity: sha512-p9syiZD1kU4I+1ya7f7g+zD1GiUvR8fdlRlNmgsZNWlyjtc8rlV2EjTLd/35x1LsdBq020GVvtzp0ZmPgBI09Q==} + '@mariozechner/clipboard-darwin-x64@0.3.9': + resolution: {integrity: sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@mariozechner/clipboard-linux-arm64-gnu@0.3.6': - resolution: {integrity: sha512-5JFf5rGofrm+V29HNF+wLthXphHdQpMbKDUYJ5tML6/Z5DLlLOV/9Ak4kDPtYyZ+Dzf+kAusE0VsFg4+tfP1IA==} + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': + resolution: {integrity: sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@mariozechner/clipboard-linux-arm64-musl@0.3.6': - resolution: {integrity: sha512-JlVjxxw0GbGC0djXYWRIqyteO3J1KZ/QG3udlEFaOD5TLOM1FnmXXAPDQBqr+aBVr720ef9K00dirYnJ0LDCtw==} + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': + resolution: {integrity: sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@mariozechner/clipboard-linux-riscv64-gnu@0.3.6': - resolution: {integrity: sha512-4t8BUi5zZ+L77otFQVnVSlaTyAX4TVk9EqQm4syMrEQp96trFEHEwwNHcNEBGzYv5+K7mxay50TthYkz47OWzQ==} + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': + resolution: {integrity: sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] libc: [glibc] - '@mariozechner/clipboard-linux-x64-gnu@0.3.6': - resolution: {integrity: sha512-trtPwcNLW37irwQCJLtCxLw757jjJZk3TSnY/MU9bhtWtA3K9b/eLW0e4RGhUXDoFRds9opNWWaUDuFLa8dm0w==} + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': + resolution: {integrity: sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@mariozechner/clipboard-linux-x64-musl@0.3.6': - resolution: {integrity: sha512-WfnzIvOCCWQiN0MmltCEo6cLceUDbYe+I7xyFZjaps5A+2Op/M2CY7Rey+C4ucQhrvmpoHmTSFgY9ODWk7snoA==} + '@mariozechner/clipboard-linux-x64-musl@0.3.9': + resolution: {integrity: sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@mariozechner/clipboard-win32-arm64-msvc@0.3.6': - resolution: {integrity: sha512-+8+1aHYsBPUjmW3otmWlg+Hijt0iJvoBBs5e0mxFeUd4gDaKMB8Bn6x7c6KVtscg7E5j5NFXnwQqNSIAO4p8zQ==} + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': + resolution: {integrity: sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@mariozechner/clipboard-win32-x64-msvc@0.3.6': - resolution: {integrity: sha512-S4xfPmERC8ZkiLHe3vekZCjdDwNEETCuvCgQK2kP6/TnvmUkq1y2Pk+DjM4t8uh9KMX9bH4zs5ePcKa8GTXmfg==} + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': + resolution: {integrity: sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@mariozechner/clipboard@0.3.6': - resolution: {integrity: sha512-MXdtr+6+ntlIVHdrZYuZNQydu6o8yZswFJ2Ln81j2O/Y9B/LDHvEaIm95xWNPkjGTWriSOeLnQJRFs6dYb60bg==} + '@mariozechner/clipboard@0.3.9': + resolution: {integrity: sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==} engines: {node: '>= 10'} - '@mistralai/mistralai@2.2.1': - resolution: {integrity: sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==} + '@mistralai/mistralai@2.2.6': + resolution: {integrity: sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true '@nodable/entities@2.1.0': resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} @@ -516,6 +521,14 @@ packages: '@octokit/types@16.0.0': resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/semantic-conventions@1.41.1': + resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} + engines: {node: '>=14'} + '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -636,6 +649,10 @@ packages: resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} engines: {node: '>=14.0.0'} + '@smithy/node-http-handler@4.7.3': + resolution: {integrity: sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==} + engines: {node: '>=18.0.0'} + '@smithy/node-http-handler@4.7.4': resolution: {integrity: sha512-HIeF+1vrDGzPkkv39Hj2vlHSXHY3p958jd/8ZnePIY6+ZOsQX8coyEUKO5yQu4r0bQIVsbpotVIrXXwyycMStQ==} engines: {node: '>=18.0.0'} @@ -644,10 +661,6 @@ packages: resolution: {integrity: sha512-e5UtkMvsatzBfbeBZjEOt0k0Z3BEsjTFL/n6fdO5vtBLe67tdy0dX7xw2DU7uZ3acwoHyeCqpU2Fzb7pxwHb6Q==} engines: {node: '>=18.0.0'} - '@smithy/types@4.14.1': - resolution: {integrity: sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==} - engines: {node: '>=18.0.0'} - '@smithy/types@4.14.2': resolution: {integrity: sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==} engines: {node: '>=18.0.0'} @@ -1182,10 +1195,6 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.5.0: - resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} - engines: {node: '>=18'} - get-east-asian-width@1.6.0: resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} @@ -1487,9 +1496,6 @@ packages: jws@4.0.1: resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} - koffi@2.16.2: - resolution: {integrity: sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA==} - lefthook-darwin-arm64@2.1.6: resolution: {integrity: sha512-hyB7eeiX78BS66f70byTJacDLC/xV1vgMv9n+idFUsrM7J3Udd/ag9Ag5NP3t0eN0EqQqAtrNnt35EH01lxnRQ==} cpu: [arm64] @@ -1640,6 +1646,11 @@ packages: engines: {node: '>= 18'} hasBin: true + marked@18.0.5: + resolution: {integrity: sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==} + engines: {node: '>= 20'} + hasBin: true + meow@12.1.1: resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} engines: {node: '>=16.10'} @@ -2107,6 +2118,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2350,8 +2366,8 @@ packages: resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} engines: {node: '>=20.18.1'} - undici@8.3.0: - resolution: {integrity: sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==} + undici@8.5.0: + resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==} engines: {node: '>=22.19.0'} unicode-emoji-modifier-base@1.0.0: @@ -2572,7 +2588,7 @@ snapshots: '@smithy/core': 3.24.4 '@smithy/fetch-http-handler': 5.4.4 '@smithy/node-http-handler': 4.7.4 - '@smithy/types': 4.14.1 + '@smithy/types': 4.14.2 tslib: 2.8.1 '@aws-sdk/core@3.974.13': @@ -2721,7 +2737,7 @@ snapshots: '@aws-sdk/nested-clients': 3.997.11 '@aws-sdk/types': 3.973.9 '@smithy/core': 3.24.4 - '@smithy/types': 4.14.1 + '@smithy/types': 4.14.2 tslib: 2.8.1 '@aws-sdk/token-providers@3.1052.0': @@ -2962,9 +2978,9 @@ snapshots: picocolors: 1.1.1 optional: true - '@earendil-works/pi-agent-core@0.75.4(ws@8.20.0)(zod@4.3.6)': + '@earendil-works/pi-agent-core@0.79.9(ws@8.20.0)(zod@4.3.6)': dependencies: - '@earendil-works/pi-ai': 0.75.4(ws@8.20.0)(zod@4.3.6) + '@earendil-works/pi-ai': 0.79.9(ws@8.20.0)(zod@4.3.6) ignore: 7.0.5 typebox: 1.1.38 yaml: 2.9.0 @@ -2976,12 +2992,14 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.75.4(ws@8.20.0)(zod@4.3.6)': + '@earendil-works/pi-ai@0.79.9(ws@8.20.0)(zod@4.3.6)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@4.3.6) '@aws-sdk/client-bedrock-runtime': 3.1048.0 '@google/genai': 1.52.0 - '@mistralai/mistralai': 2.2.1 + '@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0) + '@opentelemetry/api': 1.9.0 + '@smithy/node-http-handler': 4.7.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 openai: 6.26.0(ws@8.20.0)(zod@4.3.6) @@ -2995,11 +3013,11 @@ snapshots: - ws - zod - '@earendil-works/pi-coding-agent@0.75.4(ws@8.20.0)(zod@4.3.6)': + '@earendil-works/pi-coding-agent@0.79.9(ws@8.20.0)(zod@4.3.6)': dependencies: - '@earendil-works/pi-agent-core': 0.75.4(ws@8.20.0)(zod@4.3.6) - '@earendil-works/pi-ai': 0.75.4(ws@8.20.0)(zod@4.3.6) - '@earendil-works/pi-tui': 0.75.4 + '@earendil-works/pi-agent-core': 0.79.9(ws@8.20.0)(zod@4.3.6) + '@earendil-works/pi-ai': 0.79.9(ws@8.20.0)(zod@4.3.6) + '@earendil-works/pi-tui': 0.79.9 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 cross-spawn: 7.0.6 @@ -3011,11 +3029,12 @@ snapshots: jiti: 2.7.0 minimatch: 10.2.5 proper-lockfile: 4.1.2 + semver: 7.8.0 typebox: 1.1.38 - undici: 8.3.0 + undici: 8.5.0 yaml: 2.9.0 optionalDependencies: - '@mariozechner/clipboard': 0.3.6 + '@mariozechner/clipboard': 0.3.9 transitivePeerDependencies: - '@modelcontextprotocol/sdk' - bufferutil @@ -3024,12 +3043,10 @@ snapshots: - ws - zod - '@earendil-works/pi-tui@0.75.4': + '@earendil-works/pi-tui@0.79.9': dependencies: get-east-asian-width: 1.6.0 - marked: 15.0.12 - optionalDependencies: - koffi: 2.16.2 + marked: 18.0.5 '@google/genai@1.52.0': dependencies: @@ -3042,55 +3059,58 @@ snapshots: - supports-color - utf-8-validate - '@mariozechner/clipboard-darwin-arm64@0.3.6': + '@mariozechner/clipboard-darwin-arm64@0.3.9': optional: true - '@mariozechner/clipboard-darwin-universal@0.3.6': + '@mariozechner/clipboard-darwin-universal@0.3.9': optional: true - '@mariozechner/clipboard-darwin-x64@0.3.6': + '@mariozechner/clipboard-darwin-x64@0.3.9': optional: true - '@mariozechner/clipboard-linux-arm64-gnu@0.3.6': + '@mariozechner/clipboard-linux-arm64-gnu@0.3.9': optional: true - '@mariozechner/clipboard-linux-arm64-musl@0.3.6': + '@mariozechner/clipboard-linux-arm64-musl@0.3.9': optional: true - '@mariozechner/clipboard-linux-riscv64-gnu@0.3.6': + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': optional: true - '@mariozechner/clipboard-linux-x64-gnu@0.3.6': + '@mariozechner/clipboard-linux-x64-gnu@0.3.9': optional: true - '@mariozechner/clipboard-linux-x64-musl@0.3.6': + '@mariozechner/clipboard-linux-x64-musl@0.3.9': optional: true - '@mariozechner/clipboard-win32-arm64-msvc@0.3.6': + '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': optional: true - '@mariozechner/clipboard-win32-x64-msvc@0.3.6': + '@mariozechner/clipboard-win32-x64-msvc@0.3.9': optional: true - '@mariozechner/clipboard@0.3.6': + '@mariozechner/clipboard@0.3.9': optionalDependencies: - '@mariozechner/clipboard-darwin-arm64': 0.3.6 - '@mariozechner/clipboard-darwin-universal': 0.3.6 - '@mariozechner/clipboard-darwin-x64': 0.3.6 - '@mariozechner/clipboard-linux-arm64-gnu': 0.3.6 - '@mariozechner/clipboard-linux-arm64-musl': 0.3.6 - '@mariozechner/clipboard-linux-riscv64-gnu': 0.3.6 - '@mariozechner/clipboard-linux-x64-gnu': 0.3.6 - '@mariozechner/clipboard-linux-x64-musl': 0.3.6 - '@mariozechner/clipboard-win32-arm64-msvc': 0.3.6 - '@mariozechner/clipboard-win32-x64-msvc': 0.3.6 + '@mariozechner/clipboard-darwin-arm64': 0.3.9 + '@mariozechner/clipboard-darwin-universal': 0.3.9 + '@mariozechner/clipboard-darwin-x64': 0.3.9 + '@mariozechner/clipboard-linux-arm64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-arm64-musl': 0.3.9 + '@mariozechner/clipboard-linux-riscv64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-gnu': 0.3.9 + '@mariozechner/clipboard-linux-x64-musl': 0.3.9 + '@mariozechner/clipboard-win32-arm64-msvc': 0.3.9 + '@mariozechner/clipboard-win32-x64-msvc': 0.3.9 optional: true - '@mistralai/mistralai@2.2.1': + '@mistralai/mistralai@2.2.6(@opentelemetry/api@1.9.0)': dependencies: + '@opentelemetry/semantic-conventions': 1.41.1 ws: 8.20.0 zod: 4.3.6 zod-to-json-schema: 3.25.2(zod@4.3.6) + optionalDependencies: + '@opentelemetry/api': 1.9.0 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -3157,6 +3177,10 @@ snapshots: dependencies: '@octokit/openapi-types': 27.0.0 + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/semantic-conventions@1.41.1': {} + '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2': @@ -3322,6 +3346,12 @@ snapshots: dependencies: tslib: 2.8.1 + '@smithy/node-http-handler@4.7.3': + dependencies: + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@smithy/node-http-handler@4.7.4': dependencies: '@smithy/core': 3.24.4 @@ -3334,10 +3364,6 @@ snapshots: '@smithy/types': 4.14.2 tslib: 2.8.1 - '@smithy/types@4.14.1': - dependencies: - tslib: 2.8.1 - '@smithy/types@4.14.2': dependencies: tslib: 2.8.1 @@ -3902,8 +3928,6 @@ snapshots: get-caller-file@2.0.5: {} - get-east-asian-width@1.5.0: {} - get-east-asian-width@1.6.0: {} get-stream@6.0.1: {} @@ -4195,9 +4219,6 @@ snapshots: jwa: 2.0.1 safe-buffer: 5.2.1 - koffi@2.16.2: - optional: true - lefthook-darwin-arm64@2.1.6: optional: true @@ -4323,6 +4344,8 @@ snapshots: marked@15.0.12: {} + marked@18.0.5: {} + meow@12.1.1: {} meow@13.2.0: {} @@ -4393,7 +4416,7 @@ snapshots: normalize-package-data@8.0.0: dependencies: - hosted-git-info: 9.0.2 + hosted-git-info: 9.0.3 semver: 7.7.4 validate-npm-package-license: 3.0.4 @@ -4722,6 +4745,8 @@ snapshots: semver@7.7.4: {} + semver@7.8.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -4782,7 +4807,7 @@ snapshots: string-width@7.2.0: dependencies: emoji-regex: 10.6.0 - get-east-asian-width: 1.5.0 + get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 string_decoder@1.1.1: @@ -4928,7 +4953,7 @@ snapshots: undici@7.25.0: {} - undici@8.3.0: {} + undici@8.5.0: {} unicode-emoji-modifier-base@1.0.0: {} diff --git a/src/answer-commands.ts b/src/answer-commands.ts index 4deb064..023bb20 100644 --- a/src/answer-commands.ts +++ b/src/answer-commands.ts @@ -79,8 +79,8 @@ async function runAnswerCommand( ctx: ExtensionCommandContext, remoteAsk?: RemoteAskRuntime ): Promise { - if (!ctx.hasUI) { - ctx.ui.notify("/answer requires interactive UI.", "error"); + if (ctx.mode !== "tui") { + ctx.ui.notify("/answer requires interactive TUI mode.", "error"); return; } @@ -253,8 +253,8 @@ async function runReplayCommand( source: AskPayloadSource; } ): Promise { - if (!ctx.hasUI) { - ctx.ui.notify("Ask replay requires interactive UI.", "error"); + if (ctx.mode !== "tui") { + ctx.ui.notify("Ask replay requires interactive TUI mode.", "error"); return; } const lookup = findLatestPayloadInCurrentBranch(ctx, options.source); diff --git a/src/ask-settings-command.ts b/src/ask-settings-command.ts index 993aa88..05b348b 100644 --- a/src/ask-settings-command.ts +++ b/src/ask-settings-command.ts @@ -8,7 +8,7 @@ export function registerAskSettingsCommand(pi: ExtensionAPI) { pi.registerCommand("ask-settings", { description: "Open ask settings", handler: async (_args: string, ctx: ExtensionCommandContext) => { - await showAskSettings(ctx.ui); + await showAskSettings(ctx); }, }); } diff --git a/src/ask-tool-helpers.ts b/src/ask-tool-helpers.ts index 0db2298..796f009 100644 --- a/src/ask-tool-helpers.ts +++ b/src/ask-tool-helpers.ts @@ -153,8 +153,8 @@ function formatNonInteractiveMessage( state: ReturnType ): string { const lines = [ - "Needs user input: ask_user requires interactive UI.", - "Run same tool call in interactive session, or ask user these questions manually:", + "Needs user input: ask_user requires interactive TUI mode.", + "Run same tool call in interactive TUI mode, or ask user these questions manually:", ]; for (const [index, question] of state.questions.entries()) { diff --git a/src/ask-tool.ts b/src/ask-tool.ts index 48ea9d9..ee83ebf 100644 --- a/src/ask-tool.ts +++ b/src/ask-tool.ts @@ -67,7 +67,7 @@ async function executeAskTool( source: "tool", sourceEntryId: toolCallId, }); - if (!ctx.hasUI) { + if (ctx.mode !== "tui") { return nonInteractiveResponse(validation.state); } ctx.ui.setWorkingVisible(false); diff --git a/src/config/store.ts b/src/config/store.ts index e39c6b8..193a765 100644 --- a/src/config/store.ts +++ b/src/config/store.ts @@ -171,6 +171,10 @@ export function getAskConfigStore(): AskConfigStore { return askConfigStore; } +export function resetAskConfigStore(): void { + askConfigStore = undefined; +} + export function getAskConfigPath(): string { return join(getAgentDir(), "extensions", "eko24ive-pi-ask.json"); } diff --git a/src/index.ts b/src/index.ts index eb5efcc..cab322d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { registerAnswerCommands } from "./answer-commands.ts"; import { registerAskSettingsCommand } from "./ask-settings-command.ts"; import { registerAskTool } from "./ask-tool.ts"; +import { resetAskConfigStore } from "./config/store.ts"; import { createRemoteAskRuntime } from "./remote-ask.ts"; const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); @@ -15,10 +16,14 @@ const CONFIGURATION_DOC_PATH = resolve( const PI_ASK_CONFIG_PROMPT = `When the user asks to configure, customize, debug, or explain @eko24ive/pi-ask settings or keymaps, first read ${CONFIGURATION_DOC_PATH} and follow it as the source of truth before editing config files.`; export default function askExtension(pi: ExtensionAPI) { + resetAskConfigStore(); pi.on("before_agent_start", async (event) => ({ systemPrompt: `${event.systemPrompt}\n\n${PI_ASK_CONFIG_PROMPT}`, })); const remoteAsk = createRemoteAskRuntime(pi.events); + pi.on("session_shutdown", () => { + remoteAsk.disposeAll(); + }); registerAskTool(pi, remoteAsk); registerAskSettingsCommand(pi); registerAnswerCommands(pi, remoteAsk); diff --git a/src/remote-ask.ts b/src/remote-ask.ts index d23ccd4..4c8692b 100644 --- a/src/remote-ask.ts +++ b/src/remote-ask.ts @@ -95,6 +95,7 @@ export interface RemoteAskFlowHandle { } export interface RemoteAskRuntime { + disposeAll: () => void; startFlow: (flow: RemoteAskFlowInput) => RemoteAskFlowHandle; } @@ -102,12 +103,15 @@ type EventBus = ExtensionAPI["events"]; export function createRemoteAskRuntime(events: EventBus): RemoteAskRuntime { const activeFlows = new Map(); - - events.on(PI_ASK_SUBMIT_EVENT, (data) => { + const unsubscribeSubmit = events.on(PI_ASK_SUBMIT_EVENT, (data) => { handleSubmitEvent(events, activeFlows, data); }); return { + disposeAll() { + activeFlows.clear(); + unsubscribeSubmit(); + }, startFlow(flow) { return startRemoteAskFlow(events, activeFlows, flow); }, diff --git a/src/ui/autocomplete.ts b/src/ui/autocomplete.ts index e4be05c..c3df5e8 100644 --- a/src/ui/autocomplete.ts +++ b/src/ui/autocomplete.ts @@ -13,10 +13,13 @@ const FD_BINARY_NAMES = * themselves when reusing CombinedAutocompleteProvider for `@` file mentions. */ export function createAskAutocompleteProvider(cwd: string) { - return new CombinedAutocompleteProvider( - [], - cwd, - findAutocompleteBinary(FD_BINARY_NAMES) + return Object.assign( + new CombinedAutocompleteProvider( + [], + cwd, + findAutocompleteBinary(FD_BINARY_NAMES) + ), + { triggerCharacters: ["@"] } ); } diff --git a/src/ui/controller.ts b/src/ui/controller.ts index 7f861f8..dc76721 100644 --- a/src/ui/controller.ts +++ b/src/ui/controller.ts @@ -113,6 +113,12 @@ export async function runAskFlow( presentSingleAsMulti: options.presentSingleAsMulti ?? config.behaviour.presentSingleAsMulti, }; + if (ctx.mode !== "tui") { + return { + ...toAskResult(createInitialState(params, flowOptions)), + cancelled: true, + }; + } return ctx.ui.custom((...args) => createAskFlowController(args, { ...params, @@ -172,9 +178,15 @@ function createAskFlowController( }); return { + get focused() { + return controller.editor.focused; + }, + set focused(value: boolean) { + controller.editor.focused = value; + }, render: (width: number) => renderController(controller, width), invalidate() { - // The editor manages its own async autocomplete redraws. + controller.editor.invalidate(); }, handleInput(data: string) { handleControllerInput(controller, data); @@ -536,7 +548,7 @@ function showSettingsModal(controller: AskFlowController) { return; } controller.settingsOpen = true; - showAskSettings(controller.ctx.ui).finally(() => { + showAskSettings(controller.ctx).finally(() => { controller.settingsOpen = false; refresh(controller); }); diff --git a/src/ui/show-settings.ts b/src/ui/show-settings.ts index 815e301..4490581 100644 --- a/src/ui/show-settings.ts +++ b/src/ui/show-settings.ts @@ -3,11 +3,15 @@ import { getAskConfigPath, getAskConfigStore } from "../config/store.ts"; import { AskSettingsList } from "./settings-list.ts"; export async function showAskSettings( - ui: Pick["ui"] + ctx: Pick ): Promise { + if (ctx.mode !== "tui") { + ctx.ui.notify("/ask-settings requires interactive TUI mode.", "error"); + return; + } const store = getAskConfigStore(); const { config, notice } = await store.ensureLoaded(); - return ui.custom( + return ctx.ui.custom( (tui, theme, _keybindings, done) => new AskSettingsList(theme, { configPath: getAskConfigPath(), diff --git a/tests/answer-commands.test.ts b/tests/answer-commands.test.ts new file mode 100644 index 0000000..f06a024 --- /dev/null +++ b/tests/answer-commands.test.ts @@ -0,0 +1,47 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { registerAnswerCommands } from "../src/answer-commands.ts"; + +function registerCommands() { + const commands = new Map< + string, + { handler: (args: string, ctx: any) => Promise } + >(); + registerAnswerCommands({ + registerCommand( + name: string, + command: { handler: (args: string, ctx: any) => Promise } + ) { + commands.set(name, command); + }, + } as never); + return commands; +} + +test("answer commands do not open custom UI outside TUI mode", async () => { + const commands = registerCommands(); + const notifications: Array<{ message: string; type: string }> = []; + let customOpened = false; + const ctx = { + mode: "rpc", + ui: { + custom() { + customOpened = true; + }, + notify(message: string, type: string) { + notifications.push({ message, type }); + }, + }, + }; + + await commands.get("answer")?.handler("", ctx); + await commands.get("answer:again")?.handler("", ctx); + await commands.get("ask:replay")?.handler("", ctx); + + assert.equal(customOpened, false); + assert.deepEqual(notifications, [ + { message: "/answer requires interactive TUI mode.", type: "error" }, + { message: "Ask replay requires interactive TUI mode.", type: "error" }, + { message: "Ask replay requires interactive TUI mode.", type: "error" }, + ]); +}); diff --git a/tests/ask-settings-command.test.ts b/tests/ask-settings-command.test.ts index 2785440..8830a9b 100644 --- a/tests/ask-settings-command.test.ts +++ b/tests/ask-settings-command.test.ts @@ -37,6 +37,7 @@ test("registers /ask-settings and opens the shared settings overlay", async () = const customCalls: Array<{ options: unknown; lines: string[] }> = []; await commands.get("ask-settings")?.handler("", { + mode: "tui", ui: { custom(callback: any, options: unknown) { const done = () => { @@ -75,3 +76,40 @@ test("registers /ask-settings and opens the shared settings overlay", async () = delete process.env.PI_CODING_AGENT_DIR; await rm(agentDir, { force: true, recursive: true }); }); + +test("/ask-settings notifies outside TUI instead of opening custom UI", async () => { + const commands = new Map< + string, + { handler: (args: string, ctx: any) => Promise } + >(); + registerAskSettingsCommand({ + registerCommand( + name: string, + command: { handler: (args: string, ctx: any) => Promise } + ) { + commands.set(name, command); + }, + } as never); + + const notifications: Array<{ message: string; type: string }> = []; + let customOpened = false; + await commands.get("ask-settings")?.handler("", { + mode: "rpc", + ui: { + custom() { + customOpened = true; + }, + notify(message: string, type: string) { + notifications.push({ message, type }); + }, + }, + }); + + assert.equal(customOpened, false); + assert.deepEqual(notifications, [ + { + message: "/ask-settings requires interactive TUI mode.", + type: "error", + }, + ]); +}); diff --git a/tests/ask-tool.test.ts b/tests/ask-tool.test.ts index e987403..a5704ae 100644 --- a/tests/ask-tool.test.ts +++ b/tests/ask-tool.test.ts @@ -4,7 +4,7 @@ import { registerAskTool } from "../src/ask-tool.ts"; import type { AskParams } from "../src/types.ts"; const NON_INTERACTIVE_MESSAGE_RE = - /Needs user input: ask_user requires interactive UI\./; + /Needs user input: ask_user requires interactive TUI mode\./; const FIRST_QUESTION_RE = /1\. Goal: What should I optimize for\?/; const SPEED_OPTION_RE = /- Speed \[speed\]/; const CUSTOM_OPTION_RE = /- Type your own \[custom\]/; @@ -47,8 +47,8 @@ function registerMockTool() { }; } -function makeCtx(hasUi: boolean): unknown { - return { [HAS_UI]: hasUi }; +function makeCtx(hasUi: boolean, mode = hasUi ? "tui" : "print"): unknown { + return { [HAS_UI]: hasUi, mode }; } function sampleParams(): AskParams { @@ -113,6 +113,25 @@ test("ask tool returns pending questions in non-interactive mode", async () => { assert.match(result.content[0].text, CUSTOM_OPTION_RE); }); +test("ask tool does not open custom UI outside TUI mode", async () => { + const { tool } = registerMockTool(); + let customOpened = false; + + const result = await tool.execute("call-1", sampleParams(), undefined, noop, { + [HAS_UI]: true, + mode: "rpc", + ui: { + custom() { + customOpened = true; + }, + }, + }); + + assert.equal(customOpened, false); + assert.equal(result.details.cancelled, true); + assert.match(result.content[0].text, NON_INTERACTIVE_MESSAGE_RE); +}); + test("ask tool includes custom answer fallback for preview questions", async () => { const { tool } = registerMockTool(); diff --git a/tests/autocomplete.test.ts b/tests/autocomplete.test.ts new file mode 100644 index 0000000..b3ac898 --- /dev/null +++ b/tests/autocomplete.test.ts @@ -0,0 +1,9 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { createAskAutocompleteProvider } from "../src/ui/autocomplete.ts"; + +test("ask autocomplete explicitly triggers on file mention marker", () => { + const provider = createAskAutocompleteProvider(process.cwd()); + + assert.deepEqual(provider.triggerCharacters, ["@"]); +}); diff --git a/tests/config-store.test.ts b/tests/config-store.test.ts index 08cb2a4..846020a 100644 --- a/tests/config-store.test.ts +++ b/tests/config-store.test.ts @@ -5,7 +5,11 @@ import { dirname, join } from "node:path"; import test from "node:test"; import { DEFAULT_ASK_CONFIG } from "../src/config/defaults.ts"; import type { AskConfig } from "../src/config/schema.ts"; -import { AskConfigStore } from "../src/config/store.ts"; +import { + AskConfigStore, + getAskConfigStore, + resetAskConfigStore, +} from "../src/config/store.ts"; const DEFAULT_KEYMAPS_NOTICE_PATTERN = /Using default ask keymaps for this session/; @@ -31,6 +35,38 @@ async function makeTempPath(name: string): Promise { return join(root, "eko24ive-pi-ask.json"); } +test("resetAskConfigStore reloads the global store from disk", async () => { + const root = await import("node:fs/promises").then(({ mkdtemp }) => + mkdtemp(join(tmpdir(), "pi-ask-config-reset-")) + ); + const path = join(root, "extensions", "eko24ive-pi-ask.json"); + await mkdir(dirname(path), { recursive: true }); + await writeFile( + path, + JSON.stringify({ + ...expectedConfigFile(), + behaviour: { + ...DEFAULT_ASK_CONFIG.behaviour, + autoSubmitWhenAnsweredWithoutNotes: true, + }, + }) + ); + process.env.PI_CODING_AGENT_DIR = root; + resetAskConfigStore(); + getAskConfigStore().setConfig(DEFAULT_ASK_CONFIG); + + resetAskConfigStore(); + const result = await getAskConfigStore().ensureLoaded(); + + assert.equal( + result.config.behaviour.autoSubmitWhenAnsweredWithoutNotes, + true + ); + delete process.env.PI_CODING_AGENT_DIR; + resetAskConfigStore(); + await rm(root, { force: true, recursive: true }); +}); + test("config store writes defaults when file is missing", async () => { const path = await makeTempPath("pi-ask-config-missing-"); const store = new AskConfigStore(path); diff --git a/tests/input.test.ts b/tests/input.test.ts index 441e38f..6a78143 100644 --- a/tests/input.test.ts +++ b/tests/input.test.ts @@ -1,5 +1,6 @@ import assert from "node:assert/strict"; import test from "node:test"; +import { Editor } from "@earendil-works/pi-tui"; import { DEFAULT_ASK_CONFIG } from "../src/config/defaults.ts"; import { getAskConfigStore } from "../src/config/store.ts"; import { createInitialState } from "../src/state/create.ts"; @@ -279,6 +280,7 @@ test("custom editor submit key controls actual editor submission", async () => { const resultPromise = runAskFlow( { cwd: process.cwd(), + mode: "tui", ui: { custom(callback: (...args: unknown[]) => unknown) { return new Promise((resolve) => { @@ -331,6 +333,78 @@ function plainTheme() { }; } +test("ask flow forwards focus and invalidation to its editor", async () => { + getAskConfigStore().setConfig({ + ...DEFAULT_ASK_CONFIG, + notifications: { + ...DEFAULT_ASK_CONFIG.notifications, + enabled: false, + }, + }); + const originalInvalidate = Editor.prototype.invalidate; + let invalidateCalls = 0; + Editor.prototype.invalidate = function patchedInvalidate(this: Editor) { + invalidateCalls += 1; + return originalInvalidate.call(this); + }; + let component: + | { + focused: boolean; + handleInput(data: string): void; + invalidate(): void; + } + | undefined; + + try { + const resultPromise = runAskFlow( + { + cwd: process.cwd(), + mode: "tui", + ui: { + custom(callback: (...args: unknown[]) => unknown) { + return new Promise((resolve) => { + component = callback( + { + requestRender() { + // Rendering is not needed for this controller test. + }, + }, + plainTheme(), + {}, + resolve + ) as typeof component; + }); + }, + }, + } as never, + { + questions: [ + { + id: "q1", + prompt: "Question?", + options: [{ value: "a", label: "A" }], + }, + ], + } + ); + + await new Promise((resolve) => setImmediate(resolve)); + assert(component); + component.focused = true; + assert.equal(component.focused, true); + component.focused = false; + assert.equal(component.focused, false); + component.invalidate(); + assert.equal(invalidateCalls, 1); + component.handleInput("\x1b"); + const result = await resultPromise; + assert.equal(result.cancelled, true); + } finally { + Editor.prototype.invalidate = originalInvalidate; + getAskConfigStore().setConfig(DEFAULT_ASK_CONFIG); + } +}); + test("custom configured note shortcuts are used at runtime", () => { const navigation = createInitialState({ questions: [ diff --git a/tests/remote-ask.test.ts b/tests/remote-ask.test.ts index eba6961..51d241c 100644 --- a/tests/remote-ask.test.ts +++ b/tests/remote-ask.test.ts @@ -172,6 +172,45 @@ test("started event questions are cloned", async () => { assert.equal(questions[0].options[0].label, "Yes"); }); +test("remote runtime disposes active flows and submit listener", () => { + const bus = new TestEventBus(); + const remoteAsk = createRemoteAskRuntime(bus as never); + let submitCalls = 0; + const flow = remoteAsk.startFlow({ + source: "tool", + questions: createInitialState({ + questions: [ + { + id: "decision", + prompt: "Proceed?", + options: [{ value: "yes", label: "Yes" }], + }, + ], + }).questions, + onSubmit: () => { + submitCalls += 1; + return { ok: true }; + }, + }); + remoteAsk.disposeAll(); + + bus.emit(PI_ASK_SUBMIT_EVENT, { + version: 1, + requestId: "request-1", + flowId: flow.flowId, + response: { + kind: "answer", + answers: { decision: { values: ["yes"] } }, + }, + }); + + assert.equal(submitCalls, 0); + assert.equal( + bus.events.some((event) => event.channel === PI_ASK_SUBMIT_RESULT_EVENT), + false + ); +}); + test("remote submit resolves an active ask flow and emits lifecycle events", async () => { getAskConfigStore().setConfig({ ...DEFAULT_ASK_CONFIG, @@ -197,6 +236,7 @@ test("remote submit resolves an active ask flow and emits lifecycle events", asy const result = await runAskFlow( { cwd: process.cwd(), + mode: "tui", ui: { custom(callback: (...args: unknown[]) => unknown) { return new Promise((resolve) => {