@asterxsk/kiln 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +3 -18
  3. package/agent/extensions/AGENTS.md +68 -68
  4. package/agent/extensions/ask-user/install.ps1 +4 -0
  5. package/agent/extensions/ask-user/install.sh +3 -0
  6. package/agent/extensions/background-terminals/install.ps1 +4 -0
  7. package/agent/extensions/background-terminals/install.sh +3 -0
  8. package/agent/extensions/destructive/install.ps1 +4 -0
  9. package/agent/extensions/destructive/install.sh +3 -0
  10. package/agent/extensions/file-search/install.ps1 +4 -0
  11. package/agent/extensions/file-search/install.sh +3 -0
  12. package/agent/extensions/goal/install.ps1 +4 -0
  13. package/agent/extensions/goal/install.sh +3 -0
  14. package/agent/extensions/herdr-agent-state.ts +257 -0
  15. package/agent/extensions/kiln-update/README.md +25 -0
  16. package/agent/extensions/kiln-update/index.ts +99 -0
  17. package/agent/extensions/kiln-update/install.ps1 +27 -0
  18. package/agent/extensions/kiln-update/install.sh +24 -0
  19. package/agent/extensions/modelconf/install.ps1 +4 -0
  20. package/agent/extensions/modelconf/install.sh +3 -0
  21. package/agent/extensions/shared/install.ps1 +4 -0
  22. package/agent/extensions/shared/install.sh +3 -0
  23. package/agent/extensions/skillsconf/install.ps1 +4 -0
  24. package/agent/extensions/skillsconf/install.sh +3 -0
  25. package/agent/extensions/statusline/install.ps1 +4 -0
  26. package/agent/extensions/statusline/install.sh +3 -0
  27. package/agent/extensions/subagents/install.ps1 +4 -0
  28. package/agent/extensions/subagents/install.sh +3 -0
  29. package/agent/extensions/taste/index.ts +443 -0
  30. package/agent/extensions/taste/install.ps1 +27 -0
  31. package/agent/extensions/taste/install.sh +24 -0
  32. package/agent/extensions/todo/install.ps1 +4 -0
  33. package/agent/extensions/todo/install.sh +3 -0
  34. package/agent/extensions/trim-context/install.ps1 +4 -0
  35. package/agent/extensions/trim-context/install.sh +3 -0
  36. package/agent/version.txt +1 -0
  37. package/agent.bak.20260905-190020/AGENTS.md +67 -0
  38. package/agent.bak.20260905-190020/README.md +5 -0
  39. package/agent.bak.20260905-190020/extensions/ask-user/index.ts +418 -0
  40. package/agent.bak.20260905-190020/extensions/ask-user/install.ps1 +23 -0
  41. package/agent.bak.20260905-190020/extensions/ask-user/install.sh +21 -0
  42. package/agent.bak.20260905-190020/extensions/ask-user/package-lock.json +769 -0
  43. package/agent.bak.20260905-190020/extensions/ask-user/package.json +19 -0
  44. package/agent.bak.20260905-190020/extensions/ask-user/prompt.ts +45 -0
  45. package/agent.bak.20260905-190020/extensions/ask-user/tsconfig.json +7 -0
  46. package/agent.bak.20260905-190020/extensions/background-terminals/docs/implementation-guide.md +942 -0
  47. package/agent.bak.20260905-190020/extensions/background-terminals/index.ts +629 -0
  48. package/agent.bak.20260905-190020/extensions/background-terminals/install.ps1 +23 -0
  49. package/agent.bak.20260905-190020/extensions/background-terminals/install.sh +21 -0
  50. package/agent.bak.20260905-190020/extensions/background-terminals/package-lock.json +769 -0
  51. package/agent.bak.20260905-190020/extensions/background-terminals/package.json +17 -0
  52. package/agent.bak.20260905-190020/extensions/background-terminals/src/domain.ts +87 -0
  53. package/agent.bak.20260905-190020/extensions/background-terminals/src/manager.ts +907 -0
  54. package/agent.bak.20260905-190020/extensions/background-terminals/src/output.ts +84 -0
  55. package/agent.bak.20260905-190020/extensions/background-terminals/src/prompt.ts +142 -0
  56. package/agent.bak.20260905-190020/extensions/background-terminals/src/result-delivery.ts +27 -0
  57. package/agent.bak.20260905-190020/extensions/background-terminals/src/runtime.ts +36 -0
  58. package/agent.bak.20260905-190020/extensions/background-terminals/src/ui/output-view.ts +79 -0
  59. package/agent.bak.20260905-190020/extensions/background-terminals/src/ui/ps.ts +621 -0
  60. package/agent.bak.20260905-190020/extensions/background-terminals/src/widget-order.ts +121 -0
  61. package/agent.bak.20260905-190020/extensions/background-terminals/test/manager.test.ts +735 -0
  62. package/agent.bak.20260905-190020/extensions/background-terminals/test/output.test.ts +109 -0
  63. package/agent.bak.20260905-190020/extensions/background-terminals/test/prompt.test.ts +125 -0
  64. package/agent.bak.20260905-190020/extensions/background-terminals/test/ps.test.ts +82 -0
  65. package/agent.bak.20260905-190020/extensions/background-terminals/test/result-delivery.test.ts +44 -0
  66. package/agent.bak.20260905-190020/extensions/background-terminals/tsconfig.json +7 -0
  67. package/agent.bak.20260905-190020/extensions/destructive/README.md +34 -0
  68. package/agent.bak.20260905-190020/extensions/destructive/index.ts +124 -0
  69. package/agent.bak.20260905-190020/extensions/destructive/install.ps1 +23 -0
  70. package/agent.bak.20260905-190020/extensions/destructive/install.sh +21 -0
  71. package/agent.bak.20260905-190020/extensions/file-search/index.ts +459 -0
  72. package/agent.bak.20260905-190020/extensions/file-search/install.ps1 +23 -0
  73. package/agent.bak.20260905-190020/extensions/file-search/install.sh +21 -0
  74. package/agent.bak.20260905-190020/extensions/file-search/package-lock.json +2253 -0
  75. package/agent.bak.20260905-190020/extensions/file-search/package.json +23 -0
  76. package/agent.bak.20260905-190020/extensions/file-search/src/args.ts +122 -0
  77. package/agent.bak.20260905-190020/extensions/file-search/src/binaries.ts +422 -0
  78. package/agent.bak.20260905-190020/extensions/file-search/src/output.ts +126 -0
  79. package/agent.bak.20260905-190020/extensions/file-search/src/process.ts +146 -0
  80. package/agent.bak.20260905-190020/extensions/file-search/src/prompt.ts +52 -0
  81. package/agent.bak.20260905-190020/extensions/file-search/test/index.spec.ts +443 -0
  82. package/agent.bak.20260905-190020/extensions/file-search/tsconfig.json +7 -0
  83. package/agent.bak.20260905-190020/extensions/goal/README.md +50 -0
  84. package/agent.bak.20260905-190020/extensions/goal/index.ts +155 -0
  85. package/agent.bak.20260905-190020/extensions/goal/install.ps1 +23 -0
  86. package/agent.bak.20260905-190020/extensions/goal/install.sh +21 -0
  87. package/agent.bak.20260905-190020/extensions/kiln-update/README.md +25 -0
  88. package/agent.bak.20260905-190020/extensions/kiln-update/index.ts +99 -0
  89. package/agent.bak.20260905-190020/extensions/kiln-update/install.ps1 +23 -0
  90. package/agent.bak.20260905-190020/extensions/kiln-update/install.sh +21 -0
  91. package/agent.bak.20260905-190020/extensions/modelconf/PLAN.md +915 -0
  92. package/agent.bak.20260905-190020/extensions/modelconf/README.md +66 -0
  93. package/agent.bak.20260905-190020/extensions/modelconf/index.ts +296 -0
  94. package/agent.bak.20260905-190020/extensions/modelconf/install.ps1 +23 -0
  95. package/agent.bak.20260905-190020/extensions/modelconf/install.sh +21 -0
  96. package/agent.bak.20260905-190020/extensions/modelconf/package-lock.json +1809 -0
  97. package/agent.bak.20260905-190020/extensions/modelconf/package.json +13 -0
  98. package/agent.bak.20260905-190020/extensions/modelconf/src/fuzzy.ts +26 -0
  99. package/agent.bak.20260905-190020/extensions/modelconf/src/glob.ts +13 -0
  100. package/agent.bak.20260905-190020/extensions/modelconf/src/persistence.test.ts +46 -0
  101. package/agent.bak.20260905-190020/extensions/modelconf/src/persistence.ts +164 -0
  102. package/agent.bak.20260905-190020/extensions/modelconf/src/ui/ModelConfView.test.ts +75 -0
  103. package/agent.bak.20260905-190020/extensions/modelconf/src/ui/ModelConfView.ts +1101 -0
  104. package/agent.bak.20260905-190020/extensions/modelconf/tsconfig.json +15 -0
  105. package/agent.bak.20260905-190020/extensions/pi-web-access/CHANGELOG.md +690 -0
  106. package/agent.bak.20260905-190020/extensions/pi-web-access/LICENSE +21 -0
  107. package/agent.bak.20260905-190020/extensions/pi-web-access/README.md +470 -0
  108. package/agent.bak.20260905-190020/extensions/pi-web-access/SECURITY.md +5 -0
  109. package/agent.bak.20260905-190020/extensions/pi-web-access/activity.ts +101 -0
  110. package/agent.bak.20260905-190020/extensions/pi-web-access/auth-fetch.ts +148 -0
  111. package/agent.bak.20260905-190020/extensions/pi-web-access/banner.png +0 -0
  112. package/agent.bak.20260905-190020/extensions/pi-web-access/brightdata-unlocker.ts +272 -0
  113. package/agent.bak.20260905-190020/extensions/pi-web-access/chrome-cookies.ts +669 -0
  114. package/agent.bak.20260905-190020/extensions/pi-web-access/content-find.ts +139 -0
  115. package/agent.bak.20260905-190020/extensions/pi-web-access/credential-source.ts +191 -0
  116. package/agent.bak.20260905-190020/extensions/pi-web-access/data-uri-sanitize.ts +406 -0
  117. package/agent.bak.20260905-190020/extensions/pi-web-access/datalab-pdf-extract.ts +568 -0
  118. package/agent.bak.20260905-190020/extensions/pi-web-access/declared-web-links.ts +173 -0
  119. package/agent.bak.20260905-190020/extensions/pi-web-access/evidence/CONTRACT-EVIDENCE.md +496 -0
  120. package/agent.bak.20260905-190020/extensions/pi-web-access/evidence/contract-probe.mjs +140 -0
  121. package/agent.bak.20260905-190020/extensions/pi-web-access/exa.ts +526 -0
  122. package/agent.bak.20260905-190020/extensions/pi-web-access/extract.ts +1196 -0
  123. package/agent.bak.20260905-190020/extensions/pi-web-access/feature-config.ts +29 -0
  124. package/agent.bak.20260905-190020/extensions/pi-web-access/fetch-params.ts +111 -0
  125. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-adc.ts +298 -0
  126. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-api.ts +353 -0
  127. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-pdf-extract.ts +108 -0
  128. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-search.ts +21 -0
  129. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-url-context.ts +128 -0
  130. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-web-config.ts +101 -0
  131. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-web.ts +487 -0
  132. package/agent.bak.20260905-190020/extensions/pi-web-access/github-api.ts +197 -0
  133. package/agent.bak.20260905-190020/extensions/pi-web-access/github-extract.ts +746 -0
  134. package/agent.bak.20260905-190020/extensions/pi-web-access/github-issue-pr.ts +700 -0
  135. package/agent.bak.20260905-190020/extensions/pi-web-access/index.ts +1737 -0
  136. package/agent.bak.20260905-190020/extensions/pi-web-access/package-lock.json +5808 -0
  137. package/agent.bak.20260905-190020/extensions/pi-web-access/package.json +64 -0
  138. package/agent.bak.20260905-190020/extensions/pi-web-access/page-query.ts +96 -0
  139. package/agent.bak.20260905-190020/extensions/pi-web-access/pdf-extract.ts +409 -0
  140. package/agent.bak.20260905-190020/extensions/pi-web-access/pi-web-fetch-demo.mp4 +0 -0
  141. package/agent.bak.20260905-190020/extensions/pi-web-access/promise-try.d.ts +7 -0
  142. package/agent.bak.20260905-190020/extensions/pi-web-access/query-rewrite.ts +51 -0
  143. package/agent.bak.20260905-190020/extensions/pi-web-access/render-search-error.ts +170 -0
  144. package/agent.bak.20260905-190020/extensions/pi-web-access/rsc-extract.ts +338 -0
  145. package/agent.bak.20260905-190020/extensions/pi-web-access/search-types.ts +20 -0
  146. package/agent.bak.20260905-190020/extensions/pi-web-access/source-check.ts +282 -0
  147. package/agent.bak.20260905-190020/extensions/pi-web-access/ssrf-protection.ts +526 -0
  148. package/agent.bak.20260905-190020/extensions/pi-web-access/storage.ts +521 -0
  149. package/agent.bak.20260905-190020/extensions/pi-web-access/summary-model-scope.ts +125 -0
  150. package/agent.bak.20260905-190020/extensions/pi-web-access/test/auth-fetch.test.mjs +208 -0
  151. package/agent.bak.20260905-190020/extensions/pi-web-access/test/brightdata-unlocker.test.mjs +840 -0
  152. package/agent.bak.20260905-190020/extensions/pi-web-access/test/chrome-cookie-extraction.test.mjs +441 -0
  153. package/agent.bak.20260905-190020/extensions/pi-web-access/test/config-path.test.mjs +283 -0
  154. package/agent.bak.20260905-190020/extensions/pi-web-access/test/content-find.test.mjs +25 -0
  155. package/agent.bak.20260905-190020/extensions/pi-web-access/test/credential-source.test.mjs +118 -0
  156. package/agent.bak.20260905-190020/extensions/pi-web-access/test/data-uri-sanitize.test.mjs +210 -0
  157. package/agent.bak.20260905-190020/extensions/pi-web-access/test/datalab-pdf-extract.test.mjs +552 -0
  158. package/agent.bak.20260905-190020/extensions/pi-web-access/test/declared-web-links.test.mjs +212 -0
  159. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-answer-storage.test.mjs +40 -0
  160. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-cache-storage.test.mjs +334 -0
  161. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-content-domain-policy.test.mjs +95 -0
  162. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-modes.test.mjs +53 -0
  163. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-not-found-guidance.test.mjs +92 -0
  164. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-params.test.mjs +86 -0
  165. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-render-call.test.mjs +34 -0
  166. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-routing.test.mjs +173 -0
  167. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-adc-auth.test.mjs +257 -0
  168. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-api-transport.test.mjs +170 -0
  169. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-pdf-extract.test.mjs +133 -0
  170. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +178 -0
  171. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-web-header-overflow.test.mjs +148 -0
  172. package/agent.bak.20260905-190020/extensions/pi-web-access/test/get-search-content.test.mjs +223 -0
  173. package/agent.bak.20260905-190020/extensions/pi-web-access/test/github-extract.test.mjs +378 -0
  174. package/agent.bak.20260905-190020/extensions/pi-web-access/test/github-issue-pr.test.mjs +565 -0
  175. package/agent.bak.20260905-190020/extensions/pi-web-access/test/inline-content-config.test.mjs +99 -0
  176. package/agent.bak.20260905-190020/extensions/pi-web-access/test/lazy-extract-load.test.mjs +118 -0
  177. package/agent.bak.20260905-190020/extensions/pi-web-access/test/local-video-oversize.test.mjs +52 -0
  178. package/agent.bak.20260905-190020/extensions/pi-web-access/test/package-typebox-dependency.test.mjs +50 -0
  179. package/agent.bak.20260905-190020/extensions/pi-web-access/test/page-query.test.mjs +51 -0
  180. package/agent.bak.20260905-190020/extensions/pi-web-access/test/pdf-config.test.mjs +140 -0
  181. package/agent.bak.20260905-190020/extensions/pi-web-access/test/pdf-extract.test.mjs +500 -0
  182. package/agent.bak.20260905-190020/extensions/pi-web-access/test/proxy-transport.test.mjs +286 -0
  183. package/agent.bak.20260905-190020/extensions/pi-web-access/test/query-rewrite.test.mjs +52 -0
  184. package/agent.bak.20260905-190020/extensions/pi-web-access/test/rsc-fallback.test.mjs +102 -0
  185. package/agent.bak.20260905-190020/extensions/pi-web-access/test/search-error-render.test.mjs +152 -0
  186. package/agent.bak.20260905-190020/extensions/pi-web-access/test/search-providers.test.mjs +274 -0
  187. package/agent.bak.20260905-190020/extensions/pi-web-access/test/source-check.test.mjs +179 -0
  188. package/agent.bak.20260905-190020/extensions/pi-web-access/test/ssrf-allow-ranges-config.test.mjs +205 -0
  189. package/agent.bak.20260905-190020/extensions/pi-web-access/test/ssrf-protection.test.mjs +456 -0
  190. package/agent.bak.20260905-190020/extensions/pi-web-access/test/summary-model-scope.test.mjs +106 -0
  191. package/agent.bak.20260905-190020/extensions/pi-web-access/test/tool-registration-config.test.mjs +182 -0
  192. package/agent.bak.20260905-190020/extensions/pi-web-access/test/web-search-answer-render.test.mjs +66 -0
  193. package/agent.bak.20260905-190020/extensions/pi-web-access/test/youtube-extract-errors.test.mjs +64 -0
  194. package/agent.bak.20260905-190020/extensions/pi-web-access/tsconfig.json +11 -0
  195. package/agent.bak.20260905-190020/extensions/pi-web-access/utils.ts +451 -0
  196. package/agent.bak.20260905-190020/extensions/pi-web-access/video-extract.ts +392 -0
  197. package/agent.bak.20260905-190020/extensions/pi-web-access/youtube-extract.ts +328 -0
  198. package/agent.bak.20260905-190020/extensions/shared/activity-status.ts +31 -0
  199. package/agent.bak.20260905-190020/extensions/shared/child-session.ts +148 -0
  200. package/agent.bak.20260905-190020/extensions/shared/context-utilization.ts +47 -0
  201. package/agent.bak.20260905-190020/extensions/shared/dashboard-state.ts +99 -0
  202. package/agent.bak.20260905-190020/extensions/shared/install.ps1 +23 -0
  203. package/agent.bak.20260905-190020/extensions/shared/install.sh +21 -0
  204. package/agent.bak.20260905-190020/extensions/shared/test/child-session.test.ts +270 -0
  205. package/agent.bak.20260905-190020/extensions/shared/test/context-utilization.test.ts +48 -0
  206. package/agent.bak.20260905-190020/extensions/shared/test/tool-call-timeout.test.ts +117 -0
  207. package/agent.bak.20260905-190020/extensions/shared/test/widget-order.test.ts +167 -0
  208. package/agent.bak.20260905-190020/extensions/shared/tool-call-timeout.ts +104 -0
  209. package/agent.bak.20260905-190020/extensions/skillsconf/README.md +74 -0
  210. package/agent.bak.20260905-190020/extensions/skillsconf/index.ts +92 -0
  211. package/agent.bak.20260905-190020/extensions/skillsconf/install.ps1 +23 -0
  212. package/agent.bak.20260905-190020/extensions/skillsconf/install.sh +21 -0
  213. package/agent.bak.20260905-190020/extensions/skillsconf/package-lock.json +1809 -0
  214. package/agent.bak.20260905-190020/extensions/skillsconf/package.json +18 -0
  215. package/agent.bak.20260905-190020/extensions/skillsconf/src/delete-skill.test.ts +64 -0
  216. package/agent.bak.20260905-190020/extensions/skillsconf/src/delete-skill.ts +45 -0
  217. package/agent.bak.20260905-190020/extensions/skillsconf/src/filter.test.ts +80 -0
  218. package/agent.bak.20260905-190020/extensions/skillsconf/src/filter.ts +74 -0
  219. package/agent.bak.20260905-190020/extensions/skillsconf/src/fuzzy.ts +26 -0
  220. package/agent.bak.20260905-190020/extensions/skillsconf/src/persistence.test.ts +72 -0
  221. package/agent.bak.20260905-190020/extensions/skillsconf/src/persistence.ts +169 -0
  222. package/agent.bak.20260905-190020/extensions/skillsconf/src/ui/SkillConfView.test.ts +337 -0
  223. package/agent.bak.20260905-190020/extensions/skillsconf/src/ui/SkillConfView.ts +758 -0
  224. package/agent.bak.20260905-190020/extensions/skillsconf/src/ui/text-input.ts +91 -0
  225. package/agent.bak.20260905-190020/extensions/skillsconf/src/ui/tui-helpers.ts +144 -0
  226. package/agent.bak.20260905-190020/extensions/skillsconf/tsconfig.json +15 -0
  227. package/agent.bak.20260905-190020/extensions/statusline/index.ts +282 -0
  228. package/agent.bak.20260905-190020/extensions/statusline/install.ps1 +23 -0
  229. package/agent.bak.20260905-190020/extensions/statusline/install.sh +21 -0
  230. package/agent.bak.20260905-190020/extensions/subagents/docs/design-plan.md +568 -0
  231. package/agent.bak.20260905-190020/extensions/subagents/docs/effect-v4-extension-guide.md +354 -0
  232. package/agent.bak.20260905-190020/extensions/subagents/docs/effect-v4-notes.md +571 -0
  233. package/agent.bak.20260905-190020/extensions/subagents/index.ts +826 -0
  234. package/agent.bak.20260905-190020/extensions/subagents/install.ps1 +23 -0
  235. package/agent.bak.20260905-190020/extensions/subagents/install.sh +21 -0
  236. package/agent.bak.20260905-190020/extensions/subagents/package-lock.json +2244 -0
  237. package/agent.bak.20260905-190020/extensions/subagents/package.json +19 -0
  238. package/agent.bak.20260905-190020/extensions/subagents/src/backend.ts +73 -0
  239. package/agent.bak.20260905-190020/extensions/subagents/src/backends/claude.ts +701 -0
  240. package/agent.bak.20260905-190020/extensions/subagents/src/backends/codex.ts +1060 -0
  241. package/agent.bak.20260905-190020/extensions/subagents/src/backends/pi.ts +575 -0
  242. package/agent.bak.20260905-190020/extensions/subagents/src/backends/stub.ts +300 -0
  243. package/agent.bak.20260905-190020/extensions/subagents/src/by-the-way.ts +21 -0
  244. package/agent.bak.20260905-190020/extensions/subagents/src/domain.ts +253 -0
  245. package/agent.bak.20260905-190020/extensions/subagents/src/format.ts +74 -0
  246. package/agent.bak.20260905-190020/extensions/subagents/src/manager.ts +736 -0
  247. package/agent.bak.20260905-190020/extensions/subagents/src/prompt.ts +92 -0
  248. package/agent.bak.20260905-190020/extensions/subagents/src/result-delivery.ts +20 -0
  249. package/agent.bak.20260905-190020/extensions/subagents/src/runtime.ts +53 -0
  250. package/agent.bak.20260905-190020/extensions/subagents/src/ui/takeover.ts +583 -0
  251. package/agent.bak.20260905-190020/extensions/subagents/src/ui/transcript.ts +201 -0
  252. package/agent.bak.20260905-190020/extensions/subagents/src/widget-order.ts +121 -0
  253. package/agent.bak.20260905-190020/extensions/subagents/test/by-the-way.test.ts +29 -0
  254. package/agent.bak.20260905-190020/extensions/subagents/test/claude.test.ts +119 -0
  255. package/agent.bak.20260905-190020/extensions/subagents/test/codex.test.ts +102 -0
  256. package/agent.bak.20260905-190020/extensions/subagents/test/context-usage.test.ts +107 -0
  257. package/agent.bak.20260905-190020/extensions/subagents/test/manager.test.ts +276 -0
  258. package/agent.bak.20260905-190020/extensions/subagents/test/result-delivery.test.ts +27 -0
  259. package/agent.bak.20260905-190020/extensions/subagents/test/takeover.test.ts +29 -0
  260. package/agent.bak.20260905-190020/extensions/subagents/tsconfig.json +7 -0
  261. package/agent.bak.20260905-190020/extensions/taste/index.ts +443 -0
  262. package/agent.bak.20260905-190020/extensions/taste/install.ps1 +23 -0
  263. package/agent.bak.20260905-190020/extensions/taste/install.sh +21 -0
  264. package/agent.bak.20260905-190020/extensions/todo/AGENTS.md +38 -0
  265. package/agent.bak.20260905-190020/extensions/todo/LICENSE +21 -0
  266. package/agent.bak.20260905-190020/extensions/todo/config.ts +55 -0
  267. package/agent.bak.20260905-190020/extensions/todo/index.ts +151 -0
  268. package/agent.bak.20260905-190020/extensions/todo/install.ps1 +23 -0
  269. package/agent.bak.20260905-190020/extensions/todo/install.sh +21 -0
  270. package/agent.bak.20260905-190020/extensions/todo/locales/de.json +17 -0
  271. package/agent.bak.20260905-190020/extensions/todo/locales/en.json +15 -0
  272. package/agent.bak.20260905-190020/extensions/todo/locales/es.json +17 -0
  273. package/agent.bak.20260905-190020/extensions/todo/locales/fr.json +17 -0
  274. package/agent.bak.20260905-190020/extensions/todo/locales/pt-BR.json +17 -0
  275. package/agent.bak.20260905-190020/extensions/todo/locales/pt.json +17 -0
  276. package/agent.bak.20260905-190020/extensions/todo/locales/ru.json +17 -0
  277. package/agent.bak.20260905-190020/extensions/todo/locales/uk.json +17 -0
  278. package/agent.bak.20260905-190020/extensions/todo/locales/zh.json +17 -0
  279. package/agent.bak.20260905-190020/extensions/todo/package-lock.json +3358 -0
  280. package/agent.bak.20260905-190020/extensions/todo/package.json +67 -0
  281. package/agent.bak.20260905-190020/extensions/todo/state/i18n-bridge.ts +64 -0
  282. package/agent.bak.20260905-190020/extensions/todo/state/invariants.ts +20 -0
  283. package/agent.bak.20260905-190020/extensions/todo/state/replay.ts +38 -0
  284. package/agent.bak.20260905-190020/extensions/todo/state/selectors.ts +107 -0
  285. package/agent.bak.20260905-190020/extensions/todo/state/state-reducer.ts +326 -0
  286. package/agent.bak.20260905-190020/extensions/todo/state/state.ts +18 -0
  287. package/agent.bak.20260905-190020/extensions/todo/state/store.ts +82 -0
  288. package/agent.bak.20260905-190020/extensions/todo/state/task-graph.ts +57 -0
  289. package/agent.bak.20260905-190020/extensions/todo/todo-overlay.ts +204 -0
  290. package/agent.bak.20260905-190020/extensions/todo/todo.ts +155 -0
  291. package/agent.bak.20260905-190020/extensions/todo/tool/response-envelope.ts +109 -0
  292. package/agent.bak.20260905-190020/extensions/todo/tool/types.ts +206 -0
  293. package/agent.bak.20260905-190020/extensions/todo/view/format.ts +177 -0
  294. package/agent.bak.20260905-190020/extensions/todo/widget-order.ts +121 -0
  295. package/agent.bak.20260905-190020/extensions/trim-context/README.md +54 -0
  296. package/agent.bak.20260905-190020/extensions/trim-context/index.ts +487 -0
  297. package/agent.bak.20260905-190020/extensions/trim-context/install.ps1 +23 -0
  298. package/agent.bak.20260905-190020/extensions/trim-context/install.sh +21 -0
  299. package/agent.bak.20260905-190020/keybindings.json +7 -0
  300. package/bin/kiln.js +588 -471
  301. package/package.json +27 -27
  302. package/agent/install.ps1 +0 -637
  303. package/agent/install.sh +0 -620
@@ -0,0 +1,915 @@
1
+ # modelconf Extension Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Build a `modelconf` pi extension in its own folder (`~/.pi/agent/extensions/modelconf/`) that provides a TUI for browsing models per-provider, fuzzy-filtering, toggling hidden/visible with persisting via `enabledModels`, and bulk glob include/exclude.
6
+
7
+ **Architecture:** Single extension folder `modelconf/` with `index.ts` entry registering `/modelconf` command. A state helper reads/writes `settings.json` (`enabledModels`) and diffs against `ctx.modelRegistry.getAll()` vs `getAvailable()`. A full-screen custom TUI component (`ModelConfView`) renders provider-grouped list; sub-overlays handle `/` fuzzy search and `x` glob menu. Fuzzy matching is a local `fuzzyMatch()` (subsequence + scoring) to avoid extra deps. Glob uses `minimatch` or simple `*`->RegExp conversion. Save calls `settingsManager.setEnabledModels()` if available else fs write + `ctx.reload()`.
8
+
9
+ **Tech Stack:** TypeScript (strict), `@earendil-works/pi-coding-agent` ExtensionAPI, `@earendil-works/pi-tui` (Container, Input, theme), `typebox` for tool params (not needed), Node `fs/promises`, `os`, `path`. No new runtime deps except optional `minimatch` (prefer zero-dep includes).
10
+
11
+ ## Global Constraints
12
+
13
+ - Extension must be in its own folder named `modelconf` — discovery via `~/.pi/agent/extensions/` and `settings.json` `extensions: ["extensions/"]`.
14
+ - Must support per-provider grouping and indicate visible vs hidden per `enabledModels`.
15
+ - Fuzzy search triggered by `/`, hide/show via `Space`, save via `Enter`, bulk glob menu via `x`, close via `Esc`/`q`.
16
+ - Persistence must survive reload and affect `pi --list-models` / model picker (i.e., write to settings).
17
+ - Follow existing extension patterns in `~/.pi/agent/extensions/ask-user/index.ts` (default export function, `ctx.ui.custom` with `(tui, theme, keybindings, done)`).
18
+ - Keep extension import-light; do not add `effect` unless justified.
19
+
20
+ ---
21
+
22
+ ## File Structure
23
+
24
+ ```
25
+ ~/.pi/agent/extensions/modelconf/
26
+ ├── package.json # type: module, private, scripts: check/build, deps
27
+ ├── tsconfig.json # extends strict, outDir dist, rootDir .
28
+ ├── index.ts # Extension entry: registers /modelconf, owns state + launch
29
+ ├── src/
30
+ │ ├── persistence.ts # loadEnabledModels(), saveEnabledModels(), isVisible(), diff
31
+ │ ├── fuzzy.ts # fuzzyScore, fuzzyFilter, highlight helpers
32
+ │ ├── glob.ts # globToRegExp / matchesGlob, bulk apply helpers
33
+ │ └── ui/
34
+ │ ├── ModelConfView.ts # Main TUI Component: grouped list, key handling, renders
35
+ │ ├── SearchOverlay.ts # Optional: / search input overlay component
36
+ │ └── GlobMenu.ts # Optional: x bulk dialog (input + include/exclude choice)
37
+ └── README.md # usage / keybindings
38
+ ```
39
+
40
+ Modify (no deletion of unrelated files):
41
+ - `~/.pi/agent/settings.json` — read/write only via extension at runtime (no committed change), but plan verifies write path.
42
+
43
+ Existing patterns to reuse:
44
+ - `examples/extensions/tools.ts` — `SettingsList` toggle pattern (reference for persist + `pi.appendEntry` branch restore) — read before Task 5.
45
+ - `examples/extensions/preset.ts` — `ctx.ui.custom` + `Container` + header rendering.
46
+ - `~/.pi/agent/extensions/ask-user/index.ts` — `ctx.ui.custom` returning `{render, handleInput, invalidate}` object, `tui.requestRender()` after state change.
47
+
48
+ ---
49
+
50
+ ### Task 1: Scaffold `modelconf` extension folder
51
+
52
+ **Files:**
53
+ - Create: `./package.json`
54
+ - Create: `./tsconfig.json`
55
+ - Create: `./index.ts` (skeleton)
56
+ - Create: `./README.md`
57
+ - Create: `./src/persistence.ts` (empty stub)
58
+ - Create: `./src/fuzzy.ts` (empty stub)
59
+ - Create: `./src/glob.ts` (empty stub)
60
+ - Create: `./src/ui/ModelConfView.ts` (empty stub)
61
+
62
+ **Interfaces:**
63
+ - Consumes: none
64
+ - Produces: extension is discoverable via `/reload`, registers `/modelconf` placeholder that opens empty custom UI (used by all later tasks).
65
+
66
+ - [ ] **Step 1: Create folder structure**
67
+
68
+ ```bash
69
+ mkdir -p ~/.pi/agent/extensions/modelconf/src/ui
70
+ ```
71
+
72
+ - [ ] **Step 2: Write `package.json`**
73
+
74
+ ```json
75
+ {
76
+ "name": "modelconf",
77
+ "private": true,
78
+ "type": "module",
79
+ "version": "0.1.0",
80
+ "description": "Per-provider model visibility manager with fuzzy search and bulk glob",
81
+ "scripts": { "check": "tsc --noEmit" },
82
+ "dependencies": {},
83
+ "devDependencies": { "typescript": "^5.6.0" },
84
+ "allowScripts": {}
85
+ }
86
+ ```
87
+
88
+ - [ ] **Step 3: Write `tsconfig.json`**
89
+
90
+ ```json
91
+ {
92
+ "compilerOptions": {
93
+ "target": "ES2022",
94
+ "module": "NodeNext",
95
+ "moduleResolution": "NodeNext",
96
+ "strict": true,
97
+ "esModuleInterop": true,
98
+ "skipLibCheck": true,
99
+ "outDir": "dist",
100
+ "rootDir": ".",
101
+ "types": ["node"]
102
+ },
103
+ "include": ["index.ts", "src/**/*.ts"],
104
+ "exclude": ["dist", "node_modules"]
105
+ }
106
+ ```
107
+
108
+ - [ ] **Step 4: Write skeleton `index.ts` that registers `/modelconf` and shows placeholder UI**
109
+
110
+ ```typescript
111
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
112
+
113
+ export default function modelconf(pi: ExtensionAPI) {
114
+ pi.registerCommand("modelconf", {
115
+ description: "Manage model visibility per provider (fuzzy search: /, toggle: space, bulk: x, save: enter)",
116
+ handler: async (_args, ctx) => {
117
+ if (ctx.mode !== "tui") {
118
+ ctx.ui.notify("/modelconf requires TUI mode", "error");
119
+ return;
120
+ }
121
+ await ctx.ui.custom((tui, theme, _kb, done) => {
122
+ let closed = false;
123
+ return {
124
+ render(width: number) {
125
+ const title = theme.fg("accent", theme.bold("modelconf — scaffold"));
126
+ const hint = theme.fg("dim", "Press Esc to close · upcoming: / fuzzy, space toggle, x glob, enter save");
127
+ return [title, hint, ""];
128
+ },
129
+ invalidate() {},
130
+ handleInput(data: string) {
131
+ if (data === "\x1b" || data === "q") { if (!closed) { closed = true; done(undefined); } }
132
+ tui.requestRender();
133
+ },
134
+ };
135
+ });
136
+ },
137
+ });
138
+ }
139
+ ```
140
+
141
+ - [ ] **Step 5: Verify discovery**
142
+
143
+ Run: `pi --help` (check no load error) then inside pi run `/reload` — expect no error and `/modelconf` appears in command list. Test: `pi -e ~/.pi/agent/extensions/modelconf --help` alternative smoke.
144
+
145
+ - [ ] **Step 6: Commit**
146
+
147
+ ```
148
+ feat(modelconf): scaffold extension folder with /modelconf placeholder
149
+ ```
150
+
151
+ ---
152
+
153
+ ### Task 2: Persistence helper — map `enabledModels` to visible/hidden
154
+
155
+ **Files:**
156
+ - Create: `./src/persistence.ts`
157
+ - Modify: `./index.ts` (import helper, log counts — no UI yet)
158
+ - Test: `./src/persistence.test.ts` (manual node test script, since plan forbids one-off bash tests — write a dedicated test file)
159
+
160
+ **Interfaces:**
161
+ - Consumes: `ctx.modelRegistry.getAll(): Model[]`, `ctx.modelRegistry.getAvailable(): Model[]`, `settings.json` on disk
162
+ - Produces:
163
+ ```typescript
164
+ // persistence.ts
165
+ export type ModelId = `${string}/${string}`; // provider/modelId
166
+ export function toModelId(m: { provider: string; id: string }): ModelId;
167
+ export function loadEnabledModels(): Promise<string[] | undefined>;
168
+ export function saveEnabledModels(ids: string[] | undefined): Promise<void>;
169
+ export function isVisible(modelId: ModelId, enabled: string[] | undefined, allIds: ModelId[]): boolean;
170
+ export function getVisibilityMap(all: Array<{provider:string;id:string}>, enabled: string[]|undefined): Map<ModelId, boolean>;
171
+ export function toggleInEnabled(modelId: ModelId, enabled: string[] | undefined, allIds: ModelId[]): string[] | undefined;
172
+ // helpers for glob bulk — consumed by Task 6/8
173
+ export function applyBulk(enabled: string[]|undefined, allIds: ModelId[], matching: ModelId[], action: "include"|"exclude"): string[]|undefined;
174
+ ```
175
+
176
+ - [ ] **Step 1: Write failing test for visibility semantics**
177
+
178
+ ```typescript
179
+ // src/persistence.test.ts
180
+ import { isVisible, toModelId } from "./persistence.js";
181
+ import assert from "node:assert";
182
+ const all = ["opencode/a","opencode/b","9router/c"] as const;
183
+ // enabled undefined => all visible
184
+ assert.equal(isVisible("opencode/a", undefined, [...all] as any), true);
185
+ // empty array => none visible (pi convention: empty means none? verify — if pi treats empty as none, mirror that)
186
+ assert.equal(isVisible("opencode/a", [], [...all] as any), false);
187
+ assert.equal(isVisible("opencode/a", ["opencode/a"], [...all] as any), true);
188
+ assert.equal(isVisible("opencode/b", ["opencode/a"], [...all] as any), false);
189
+ console.log("persistence test pass");
190
+ ```
191
+
192
+ - [ ] **Step 2: Run test to verify it fails** (module not implemented)
193
+
194
+ Run: `npx tsc --noEmit` then `node --loader tsx src/persistence.test.ts` — expected FAIL `cannot find module`.
195
+
196
+ - [ ] **Step 3: Implement `src/persistence.ts` minimal**
197
+
198
+ ```typescript
199
+ import fs from "node:fs/promises";
200
+ import os from "node:os";
201
+ import path from "node:path";
202
+
203
+ export type ModelId = `${string}/${string}`;
204
+
205
+ export function toModelId(m: { provider: string; id: string }): ModelId {
206
+ return `${m.provider}/${m.id}` as ModelId;
207
+ }
208
+
209
+ function settingsPath(): string {
210
+ // Global settings — matches pi default. Project-local .pi/settings.json intentionally not handled in v1; document it.
211
+ return path.join(os.homedir(), ".pi", "agent", "settings.json");
212
+ }
213
+
214
+ export async function loadEnabledModels(): Promise<string[] | undefined> {
215
+ try {
216
+ const raw = await fs.readFile(settingsPath(), "utf8");
217
+ const j = JSON.parse(raw) as { enabledModels?: string[] };
218
+ return j.enabledModels;
219
+ } catch { return undefined; }
220
+ }
221
+
222
+ export async function saveEnabledModels(ids: string[] | undefined): Promise<void> {
223
+ const p = settingsPath();
224
+ const raw = await fs.readFile(p, "utf8");
225
+ const j = JSON.parse(raw) as Record<string, unknown>;
226
+ if (ids === undefined) delete j.enabledModels;
227
+ else j.enabledModels = ids;
228
+ await fs.writeFile(p, JSON.stringify(j, null, 2) + "\n", "utf8");
229
+ }
230
+
231
+ export function isVisible(modelId: ModelId, enabled: string[] | undefined, _allIds: ModelId[]): boolean {
232
+ if (enabled === undefined) return true; // no filter => all visible
233
+ return enabled.includes(modelId);
234
+ }
235
+
236
+ export function getVisibilityMap(all: Array<{provider:string;id:string}>, enabled: string[]|undefined): Map<ModelId, boolean> {
237
+ const map = new Map<ModelId, boolean>();
238
+ const allIds = all.map(toModelId);
239
+ for (const m of all) map.set(toModelId(m), isVisible(toModelId(m), enabled, allIds));
240
+ return map;
241
+ }
242
+
243
+ export function toggleInEnabled(modelId: ModelId, enabled: string[] | undefined, allIds: ModelId[]): string[] | undefined {
244
+ if (enabled === undefined) {
245
+ // First toggle while all visible => materialize as "all except one hidden"
246
+ return allIds.filter(id => id !== modelId);
247
+ }
248
+ if (enabled.includes(modelId)) return enabled.filter(id => id !== modelId);
249
+ return [...enabled, modelId];
250
+ }
251
+
252
+ export function applyBulk(enabled: string[]|undefined, allIds: ModelId[], matching: ModelId[], action: "include"|"exclude"): string[]|undefined {
253
+ if (action === "include") {
254
+ const base = enabled === undefined ? [...allIds] : [...enabled];
255
+ const set = new Set(base);
256
+ for (const id of matching) set.add(id);
257
+ const arr = [...set];
258
+ // if all visible, store undefined to keep settings clean
259
+ if (arr.length === allIds.length) return undefined;
260
+ return arr;
261
+ } else {
262
+ if (enabled === undefined) return allIds.filter(id => !matching.includes(id));
263
+ return enabled.filter(id => !matching.includes(id));
264
+ }
265
+ }
266
+ ```
267
+
268
+ Note: Verify against real pi semantics for `enabledModels: []` vs `undefined` by reading `SettingsManager.getEnabledModels()` impl in `dist/modes/interactive/interactive-mode.js:4184` — if empty means "filter to none", the above is correct; if empty is treated as "no filter", adjust `isVisible`.
269
+
270
+ - [ ] **Step 4: Run test again — expect PASS**
271
+
272
+ Run: `npx tsc --noEmit` → PASS, then `node --loader tsx src/persistence.test.ts`.
273
+
274
+ - [ ] **Step 5: Wire a debug log into `index.ts` handler**
275
+
276
+ In `/modelconf` handler, before `ctx.ui.custom`, do:
277
+ ```typescript
278
+ const all = ctx.modelRegistry.getAll().map(m => ({ provider: (m as any).provider, id: m.id }));
279
+ const avail = ctx.modelRegistry.getAvailable().map(m => `${(m as any).provider}/${m.id}`);
280
+ console.log(`[modelconf] all=${all.length} avail=${avail.length}`);
281
+ ```
282
+ Verify in pi logs.
283
+
284
+ - [ ] **Step 6: Commit**
285
+
286
+ ```
287
+ feat(modelconf): add persistence helper for enabledModels mapping
288
+ ```
289
+
290
+ ---
291
+
292
+ ### Task 3: Fuzzy search utility (`/`)
293
+
294
+ **Files:**
295
+ - Create: `./src/fuzzy.ts`
296
+ - Test: `./src/fuzzy.test.ts`
297
+
298
+ **Interfaces:**
299
+ - Consumes: array of `ModelId` strings
300
+ - Produces:
301
+ ```typescript
302
+ export function fuzzyScore(query: string, target: string): number | null; // null = no match
303
+ export function fuzzyFilter<T>(query: string, items: T[], key: (t:T)=>string): T[]; // sorted by score
304
+ export function highlightMatch(target: string, query: string, theme: any): string; // optional
305
+ ```
306
+
307
+ - [ ] **Step 1: Write failing test**
308
+
309
+ ```typescript
310
+ import { fuzzyScore, fuzzyFilter } from "./fuzzy.js";
311
+ import assert from "node:assert";
312
+ assert.ok(fuzzyScore("gpl", "gpt-5.6-luna") === null);
313
+ assert.ok(typeof fuzzyScore("lun", "gpt-5.6-luna") === "number");
314
+ const out = fuzzyFilter("lun", [{id:"gpt-5.6-luna"},{id:"minimax-m3"}], x=>x.id);
315
+ assert.equal(out[0].id, "gpt-5.6-luna");
316
+ console.log("fuzzy pass");
317
+ ```
318
+
319
+ - [ ] **Step 2: Run to confirm fail**
320
+ - [ ] **Step 3: Implement subsequence scorer (no deps)**
321
+
322
+ ```typescript
323
+ export function fuzzyScore(query: string, target: string): number | null {
324
+ if (!query) return 0;
325
+ const q = query.toLowerCase();
326
+ const t = target.toLowerCase();
327
+ let qi = 0, ti = 0, score = 0, last = -2;
328
+ for (; qi < q.length && ti < t.length; ti++) {
329
+ if (q[qi] === t[ti]) {
330
+ // bonus for contiguous + start-of-word
331
+ score += (ti === last + 1 ? 2 : 1);
332
+ if (ti === 0 || t[ti-1] === "/" || t[ti-1] === "-" || t[ti-1] === "_") score += 2;
333
+ last = ti; qi++;
334
+ }
335
+ }
336
+ if (qi !== q.length) return null;
337
+ // penalize longer targets slightly
338
+ score -= Math.floor(t.length / 20);
339
+ return score;
340
+ }
341
+
342
+ export function fuzzyFilter<T>(query: string, items: T[], key: (t:T)=>string): T[] {
343
+ if (!query.trim()) return items;
344
+ const scored = items.map(i => ({ i, s: fuzzyScore(query, key(i)) }))
345
+ .filter(x => x.s !== null) as Array<{i:T; s:number}>;
346
+ scored.sort((a,b)=> b.s - a.s);
347
+ return scored.map(x=>x.i);
348
+ }
349
+ ```
350
+
351
+ - [ ] **Step 4: Run test pass + check `npx tsc --noEmit`**
352
+
353
+ - [ ] **Step 5: Commit**
354
+
355
+ ```
356
+ feat(modelconf): add fuzzyScore / fuzzyFilter utility
357
+ ```
358
+
359
+ ---
360
+
361
+ ### Task 4: Glob helper (`x` bulk)
362
+
363
+ **Files:**
364
+ - Create: `./src/glob.ts`
365
+ - Test: `./src/glob.test.ts`
366
+
367
+ **Interfaces:**
368
+ - Produces:
369
+ ```typescript
370
+ export function matchesGlob(input: string, pattern: string): boolean;
371
+ export function filterByGlob<T>(pattern: string, items: T[], key:(t:T)=>string): T[];
372
+ ```
373
+
374
+ - [ ] **Step 1: Test**
375
+
376
+ ```typescript
377
+ import { matchesGlob } from "./glob.js";
378
+ import assert from "node:assert";
379
+ assert.equal(matchesGlob("gpt-5.6-luna", "*luna*"), true);
380
+ assert.equal(matchesGlob("gpt-5.6-luna", "gpt*"), true);
381
+ assert.equal(matchesGlob("minimax-m3", "*luna*"), false);
382
+ assert.equal(matchesGlob("a/b/c", "*"), true);
383
+ console.log("glob pass");
384
+ ```
385
+
386
+ - [ ] **Step 2: Implement (zero-dep, * and ? plus case-insensitive includes fallback)**
387
+
388
+ ```typescript
389
+ export function globToRegExp(pattern: string): RegExp {
390
+ // Support keyword without * as implicit *keyword* (per spec: "glob a keyword")
391
+ let p = pattern;
392
+ if (!p.includes("*") && !p.includes("?")) p = `*${p}*`;
393
+ const esc = p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
394
+ return new RegExp(`^${esc}$`, "i");
395
+ }
396
+ export function matchesGlob(input: string, pattern: string): boolean {
397
+ return globToRegExp(pattern).test(input);
398
+ }
399
+ export function filterByGlob<T>(pattern: string, items: T[], key:(t:T)=>string): T[] {
400
+ const rx = globToRegExp(pattern);
401
+ return items.filter(i => rx.test(key(i)));
402
+ }
403
+ ```
404
+
405
+ Document that `*` semantics covers the spec's "keyword" as substring.
406
+
407
+ - [ ] **Step 3: Verify pass + tsc**
408
+ - [ ] **Step 4: Commit**
409
+
410
+ ```
411
+ feat(modelconf): add glob helper for bulk keyword matching
412
+ ```
413
+
414
+ ---
415
+
416
+ ### Task 5: Main TUI view — per-provider grouping + navigation
417
+
418
+ **Files:**
419
+ - Create: `./src/ui/ModelConfView.ts`
420
+ - Modify: `./index.ts` (delegate to ModelConfView)
421
+ - Test: manual TUI test (`/modelconf` visual)
422
+
423
+ **Interfaces:**
424
+ - Consumes: `persistence` helpers, `fuzzy` helpers, `glob` helpers, `ExtensionContext` modelRegistry + `loadEnabledModels`
425
+ - Produces: class `ModelConfView implements Component` with contract:
426
+
427
+ ```typescript
428
+ export interface ModelRow { id: string; provider: string; modelId: string; visible: boolean; }
429
+ export class ModelConfView implements Component {
430
+ constructor(opts: {
431
+ tui: TUI; theme: Theme; keybindings: KeybindingsManager;
432
+ allModels: Array<{provider:string; id:string; name?:string}>;
433
+ visibleSet: Set<string>;
434
+ enabledRaw: string[]|undefined;
435
+ onDone: (saved: boolean)=>void;
436
+ onPersist: (nextEnabled: string[]|undefined)=>Promise<void>;
437
+ });
438
+ render(width:number): string[];
439
+ handleInput(data:string): void;
440
+ invalidate(): void;
441
+ }
442
+ ```
443
+
444
+ Rendering rules:
445
+ - Group by `provider` sorted alphabetically; providers collapsible? v1: all expanded, header line per provider: `▸ provider (3/10 visible)` in accent color.
446
+ - Rows: ` [x] modelId — name` where `[x]` is `■`/`□` or `☑`/`☐` with `visible ? success : dim`. Use `theme.fg`.
447
+ - Cursor line highlighted with `theme.bg("selection")` or `theme.fg("accent")`.
448
+ - Footer legend: `/ search space toggle x glob enter save esc cancel ↑↓ navigate`.
449
+ - Limit height to `tui` height minus chrome; handle scrolling window.
450
+
451
+ Key handling (for this task — aiming for navigation only, search/toggle/bulk deferred):
452
+ - `j`/`ArrowDown`, `k`/`ArrowUp` move cursor (also handle Kitty keys from `keybindings`? check `Keyboard`).
453
+ - `g`/`Home` top, `G`/`End` bottom.
454
+
455
+ - [ ] **Step 1: Read reference implementations**
456
+
457
+ Open `examples/extensions/tools.ts` and `examples/extensions/preset.ts` and `src/persistence.ts` to confirm model id shape: `(m as any).provider` fallback to `m.provider` or `"unknown"`.
458
+
459
+ - [ ] **Step 2: Write failing visual placeholder — launch ModelConfView from index.ts**
460
+
461
+ In `index.ts` handler, construct `allModels` via:
462
+ ```typescript
463
+ const allModels = ctx.modelRegistry.getAll().map(m => ({
464
+ provider: (m as any).provider ?? "unknown",
465
+ id: m.id,
466
+ name: (m as any).name ?? m.id,
467
+ }));
468
+ const enabledRaw = await loadEnabledModels();
469
+ const map = getVisibilityMap(allModels, enabledRaw);
470
+ ```
471
+
472
+ Pass to `new ModelConfView({ ... })` and return its `{render, handleInput, invalidate}`.
473
+
474
+ - [ ] **Step 3: Implement ModelConfView — grouped list + scroll + selection**
475
+
476
+ Pseudocode for `render(width)`:
477
+ ```typescript
478
+ render(width:number): string[] {
479
+ const lines: string[] = [];
480
+ lines.push(theme.fg("accent", theme.bold("modelconf")) + theme.fg("dim", ` ${this.rows.length} models ${this.visibleCount} visible`));
481
+ lines.push(theme.fg("dim", "─".repeat(width)));
482
+ for (const provider of this.sortedProviders) {
483
+ const group = this.rowsByProvider.get(provider)!;
484
+ const visibleInGroup = group.filter(r=>r.visible).length;
485
+ lines.push(theme.fg(provider === this.currentProvider ? "accent" : "muted",
486
+ `${this.isProviderCollapsed(provider) ? "▸" : "▾"} ${provider} ${visibleInGroup}/${group.length}`));
487
+ if (this.isProviderCollapsed(provider)) continue;
488
+ for (const row of group) {
489
+ const isCursor = row === this.cursorRow();
490
+ const mark = row.visible ? theme.fg("success", "◉") : theme.fg("dim", "○");
491
+ const label = `${mark} ${row.modelId}`.padEnd(40) + theme.fg("dim", row.name ?? "");
492
+ const line = isCursor ? theme.bg("selection", label) : label;
493
+ lines.push(theme.fg(isCursor ? "accent" : undefined as any, ` ${line}`));
494
+ }
495
+ }
496
+ lines.push("", theme.fg("dim", "/ search space toggle x bulk enter save esc cancel"));
497
+ return lines.map(l => truncateToWidth(l, width));
498
+ }
499
+ ```
500
+
501
+ Cursor math: flat index over `this.flatRows` (filtered view later expands to include search).
502
+
503
+ `handleInput` v1: only up/down/g/q/esc; other keys `done(false)` on esc.
504
+
505
+ Call `tui.requestRender()` after any state change.
506
+
507
+ - [ ] **Step 4: Test in pi**
508
+
509
+ Commands:
510
+ ```bash
511
+ pi
512
+ /modelconf
513
+ ```
514
+ Verify:
515
+ - Providers appear grouped.
516
+ - Counts correct vs `cat ~/.pi/agent/settings.json | grep enabledModels`.
517
+ - Arrow keys move highlight, no crash on long list (test with `oc` provider with many models).
518
+
519
+ - [ ] **Step 5: Handle `width` overflow — truncate row labels to `width-6`**
520
+
521
+ Use `truncateToWidth` from `@earendil-works/pi-tui`.
522
+
523
+ - [ ] **Step 6: Commit**
524
+
525
+ ```
526
+ feat(modelconf): main TUI view with per-provider grouping and navigation
527
+ ```
528
+
529
+ ---
530
+
531
+ ### Task 6: Toggle (`Space`) + Save (`Enter`)
532
+
533
+ **Files:**
534
+ - Modify: `./src/ui/ModelConfView.ts`
535
+ - Modify: `./src/persistence.ts` (if toggle semantics need fix)
536
+ - Modify: `./index.ts` (persist callback)
537
+
538
+ **Interfaces:**
539
+ - Consumes: `toggleInEnabled`, `saveEnabledModels`
540
+ - Produces: updated `enabledRaw` inside ModelConfView, flush on Enter.
541
+
542
+ Behavior:
543
+ - `Space` toggles current row's `visible`; updates internal `draftEnabled` (copy of loaded `enabledRaw` mutated via `toggleInEnabled`). Recomputes `visibleSet`. Does not yet write to disk.
544
+ - Dirty indicator: title shows `● unsaved` when `draftEnabled !== original`.
545
+ - `Enter` calls `onPersist(draftEnabled)` → `saveEnabledModels` → `ctx.ui.notify("Saved N visible", "success")` → optional `ctx.reload()` to refresh model picker. Then `done(true)`.
546
+ - `Esc` discards if dirty: prompt confirm via inline footer "Discard changes? (y/N)" — simplest: `Esc` when dirty shows confirmation overlay; second `Esc` discards, `y` discards.
547
+
548
+ - [ ] **Step 1: Write unit test for draft toggling**
549
+
550
+ ```typescript
551
+ import { toggleInEnabled } from "./persistence.js";
552
+ let draft: string[]|undefined = undefined;
553
+ const all = ["a/x","a/y","b/z"] as any;
554
+ draft = toggleInEnabled("a/x", draft, all);
555
+ assert.deepEqual(draft, ["a/y","b/z"]); // all-except-x
556
+ draft = toggleInEnabled("a/x", draft, all);
557
+ assert.ok(draft!.includes("a/x"));
558
+ console.log("toggle draft pass");
559
+ ```
560
+
561
+ - [ ] **Step 2: Implement Space handling in ModelConfView.handleInput**
562
+
563
+ ```typescript
564
+ if (data === " ") {
565
+ const row = this.cursorRow(); if (!row) return;
566
+ const modelId = `${row.provider}/${row.modelId}` as ModelId;
567
+ this.draftEnabled = toggleInEnabled(modelId, this.draftEnabled, this.allIds);
568
+ this.rebuildVisibility();
569
+ tui.requestRender();
570
+ return;
571
+ }
572
+ if (data === "\r" || data === "\n") { // Enter
573
+ await this.opts.onPersist(this.draftEnabled);
574
+ this.opts.onDone(true);
575
+ return;
576
+ }
577
+ ```
578
+
579
+ - [ ] **Step 3: Implement dirty check + footer hint**
580
+
581
+ ```typescript
582
+ private isDirty(): boolean {
583
+ const a = JSON.stringify(this.originalEnabled ?? null);
584
+ const b = JSON.stringify(this.draftEnabled ?? null);
585
+ return a !== b;
586
+ }
587
+ ```
588
+
589
+ Render footer as `theme.fg(isDirty?"warning":"dim", isDirty ? "● unsaved — press Enter to save" : "enter save esc close")`.
590
+
591
+ - [ ] **Step 4: Implement persist in `index.ts`**
592
+
593
+ ```typescript
594
+ onPersist: async (next) => {
595
+ await saveEnabledModels(next);
596
+ ctx.ui.notify(`modelconf: saved ${next === undefined ? allModels.length : next.length} enabled models — reload to apply`, "info");
597
+ // Try graceful reload; ignore if ctx.reload unavailable
598
+ try { await (ctx as any).reload?.(); } catch {}
599
+ }
600
+ ```
601
+
602
+ - [ ] **Step 5: Manual test — toggle loop**
603
+
604
+ In pi: `/modelconf`, press Space on 2 models, verify mark flips, footer shows unsaved, Enter saves, check `cat ~/.pi/agent/settings.json`, reopen `/modelconf` confirms persisted.
605
+
606
+ - [ ] **Step 6: Commit**
607
+
608
+ ```
609
+ feat(modelconf): space toggles visibility and enter persists enabledModels
610
+ ```
611
+
612
+ ---
613
+
614
+ ### Task 7: Fuzzy search (`/`)
615
+
616
+ **Files:**
617
+ - Modify: `./src/ui/ModelConfView.ts`
618
+ - Create: `./src/ui/SearchOverlay.ts` (optional inline input; can be inline in ModelConfView)
619
+ - Modify: `./index.ts` (no change, logic inside view)
620
+
621
+ **Interfaces:**
622
+ - Consumes: `fuzzyFilter` from `fuzzy.ts`
623
+ - Produces: search mode inside ModelConfView (no new external API).
624
+
625
+ Behavior:
626
+ - Pressing `/` enters search mode: an `Input` (from `@earendil-works/pi-tui`) appears at top/bottom, focused, with IME support (`Focusable` container — propagate `focused` to child Input per `docs/tui.md` container note).
627
+ - Typing filters `flatRows` via `fuzzyFilter(query, flatRows, r=>`${r.provider}/${r.modelId} ${r.name}`)`. Cursor resets to 0, scroll resets.
628
+ - `Esc` exits search (clears query), `Enter` keeps filtered view but exits input focus, Backspace edits query.
629
+ - Highlight matched rows? v1: just filtered list; v2 bonus: dim non-matching.
630
+ - Empty query returns full provider-grouped view.
631
+
632
+ - [ ] **Step 1: Verify docs/tui.md IME note — container must implement Focusable and propagate to Input**
633
+
634
+ Code sketch:
635
+
636
+ ```typescript
637
+ import { Input, Container, type Focusable } from "@earendil-works/pi-tui";
638
+
639
+ class SearchOverlay extends Container implements Focusable {
640
+ input = new Input({ placeholder: "fuzzy search…", onChange: ()=> this.opts.onChange(this.input.value) });
641
+ private _focused = false;
642
+ get focused(){ return this._focused; }
643
+ set focused(v:boolean){ this._focused = v; this.input.focused = v; }
644
+ }
645
+ ```
646
+
647
+ Or simpler: keep search as inline string + handleInput char insertion without full Input component (avoid IME complexity) — decision: use Input for correctness.
648
+
649
+ - [ ] **Step 2: Add state to ModelConfView**
650
+
651
+ ```typescript
652
+ private searchActive = false;
653
+ private searchQuery = "";
654
+ private searchInput = new Input();
655
+ private filteredRows: ModelRow[] | null = null;
656
+ ```
657
+
658
+ In `render`, if `searchActive`, prepend line: `theme.fg("accent", "/") + searchInput.render(...)`.
659
+
660
+ - [ ] **Step 3: Handle `/` key to enter search**
661
+
662
+ ```typescript
663
+ if (!this.searchActive && data === "/") {
664
+ this.searchActive = true;
665
+ this.searchInput.focused = true;
666
+ this.searchInput.value = "";
667
+ tui.requestRender(); return;
668
+ }
669
+ if (this.searchActive) {
670
+ if (data === "\x1b") { this.searchActive = false; this.searchInput.focused = false; this.filteredRows = null; this.searchQuery=""; }
671
+ else if (data === "\r") { this.searchActive = false; this.searchInput.focused = false; }
672
+ else { this.searchInput.handleInput?.(data); this.searchQuery = this.searchInput.value; this.filteredRows = fuzzyFilter(this.searchQuery, this.flatRows, r=>`${r.provider}/${r.modelId}`); this.cursorIndex = 0; }
673
+ tui.requestRender(); return;
674
+ }
675
+ ```
676
+
677
+ Route `handleInput` early to `searchInput` when active.
678
+
679
+ - [ ] **Step 4: Update rendering to use `filteredRows ?? flatRows` for flat navigation, but keep provider group rendering filtered (hide empty groups)**
680
+
681
+ In `render`, compute `rowsToShow = this.filteredRows ?? this.flatRows` but group by provider from that set.
682
+
683
+ - [ ] **Step 5: Manual test**
684
+
685
+ `/modelconf` → `/` → type `luna` → expect only `gpt-5.6-luna` etc remain; `Esc` returns; arrow navigation works within filtered.
686
+
687
+ - [ ] **Step 6: Commit**
688
+
689
+ ```
690
+ feat(modelconf): fuzzy search on / with Input overlay
691
+ ```
692
+
693
+ ---
694
+
695
+ ### Task 8: Bulk glob menu (`x`)
696
+
697
+ **Files:**
698
+ - Modify: `./src/ui/ModelConfView.ts`
699
+ - Create: `./src/ui/GlobMenu.ts`
700
+ - Modify: `./src/persistence.ts` (already has `applyBulk`, now used)
701
+ - Test: manual TUI
702
+
703
+ **Interfaces:**
704
+ - Consumes: `filterByGlob`, `applyBulk`, `loadEnabledModels`
705
+ - Produces: overlay dialog returned from `handleInput` when `x` pressed.
706
+
707
+ Behavior:
708
+ - `x` opens a centered modal (`Container` with bordered box) containing:
709
+ - Input line: `Glob/keyword: [________]` (placeholder `e.g. *luna* or luna`)
710
+ - Count preview live: `Matches 12 models`
711
+ - Two actions: `Include all matches` (enable) and `Exclude all matches` (disable), selectable via `Tab`/`Arrow` or `i`/`e` shortcuts, `Enter` confirms, `Esc` cancels.
712
+ - Footer: `Enter confirm Esc cancel Tab switch action`
713
+ - Pattern semantics: if pattern lacks `*`/`?`, treat as `*pattern*` (keyword substring, case-insensitive). This satisfies spec's "glob a keyword".
714
+ - On confirm, compute `matching = filterByGlob(pattern, allRows, r=>`${r.provider}/${r.modelId}`)` → `modelId`s, then `draftEnabled = applyBulk(draftEnabled, allIds, matchingIds, action)` → rebuild visibility → close modal → show transient notify `Included 12 / Excluded 5`.
715
+ - Does not auto-save; dirties draft like Space toggles (so Enter still needed to persist).
716
+
717
+ Modal implementation notes:
718
+ - Use `Container` + simple box drawing via `theme.fg("border", ...)` (check `examples/extensions/qna.ts` BorderedLoader pattern). Or reuse `SelectList` for action choice.
719
+ - Modal captures all input until closed (set `inGlobMenu = true` flag to route `handleInput` to modal).
720
+
721
+ - [ ] **Step 1: Write unit test for bulk include/exclude over keyword**
722
+
723
+ ```typescript
724
+ import { filterByGlob } from "./glob.js";
725
+ import { applyBulk } from "./persistence.js";
726
+ const all = ["a/luna-1","a/luna-2","a/other"] as any;
727
+ const m = filterByGlob("luna", all.map(id=>({id})), x=>x.id);
728
+ assert.equal(m.length, 2);
729
+ let draft: string[]|undefined = undefined;
730
+ draft = applyBulk(draft, all as any, ["a/luna-1","a/luna-2"] as any, "exclude");
731
+ assert.deepEqual(draft, ["a/other"]);
732
+ console.log("bulk pass");
733
+ ```
734
+
735
+ - [ ] **Step 2: Implement GlobMenu component**
736
+
737
+ ```typescript
738
+ export class GlobMenu extends Container implements Focusable {
739
+ input = new Input({ placeholder: "*luna* or luna" });
740
+ action: "include"|"exclude" = "exclude";
741
+ matches: ModelRow[] = [];
742
+ constructor(private opts: { allRows: ModelRow[]; onConfirm: (pattern:string, action:"include"|"exclude", matches:ModelRow[])=>void; onCancel: ()=>void; tui: TUI; theme: any; }) { super(); this.addChild(this.input); }
743
+ // Focusable propagation...
744
+ handleInput(data:string){ ... }
745
+ render(width:number): string[] { /* bordered box 60 chars wide centered */ }
746
+ }
747
+ ```
748
+
749
+ Simpler v1: keep menu logic inside `ModelConfView` without separate file — inline state:
750
+
751
+ ```typescript
752
+ private globOpen = false;
753
+ private globPattern = "";
754
+ private globAction: "include"|"exclude" = "exclude";
755
+ private globInput = new Input();
756
+ ```
757
+
758
+ Render overlay as centered box when `globOpen`.
759
+
760
+ - [ ] **Step 3: Wire `x` key in ModelConfView.handleInput**
761
+
762
+ ```typescript
763
+ if (!this.globOpen && data === "x") {
764
+ this.globOpen = true; this.globInput.focused = true; this.globInput.value=""; this.globPattern=""; this.globAction="exclude";
765
+ tui.requestRender(); return;
766
+ }
767
+ if (this.globOpen) {
768
+ if (data === "\x1b") { this.globOpen=false; this.globInput.focused=false; tui.requestRender(); return; }
769
+ if (data === "\t") { this.globAction = this.globAction==="exclude" ? "include" : "exclude"; tui.requestRender(); return; }
770
+ if (data === "i") { this.globAction="include"; tui.requestRender(); return; }
771
+ if (data === "e") { this.globAction="exclude"; tui.requestRender(); return; }
772
+ if (data === "\r") {
773
+ const pattern = this.globInput.value.trim();
774
+ if (!pattern) return;
775
+ const matches = filterByGlob(pattern, this.flatRows, r=>`${r.provider}/${r.modelId}`);
776
+ this.draftEnabled = applyBulk(this.draftEnabled, this.allIds, matches.map(r=>`${r.provider}/${r.modelId}` as any), this.globAction);
777
+ this.rebuildVisibility(); this.globOpen=false; this.globInput.focused=false;
778
+ tui.requestRender(); return;
779
+ }
780
+ this.globInput.handleInput?.(data); tui.requestRender(); return;
781
+ }
782
+ ```
783
+
784
+ Preview count: `const preview = filterByGlob(this.globInput.value || "*", this.flatRows, ...).length`.
785
+
786
+ - [ ] **Step 4: Render modal overlay**
787
+
788
+ In `render`, if `globOpen`, return overlay lines instead of main list OR composite: render main list dimmed then boxed menu. Simplest: return menu box full-screen centered:
789
+
790
+ ```typescript
791
+ if (this.globOpen) {
792
+ const count = filterByGlob(this.globInput.value||"", this.flatRows, r=>`${r.provider}/${r.modelId}`).length;
793
+ return [
794
+ theme.fg("accent", theme.bold("Bulk glob — include / exclude")),
795
+ theme.fg("dim", "Keyword or glob pattern (e.g. luna or *luna*)"),
796
+ `> ${this.globInput.render?.(width) ?? this.globInput.value}_`,
797
+ theme.fg("dim", `Matches: ${count} models`),
798
+ "",
799
+ this.globAction==="include" ? theme.fg("success", "▶ Include (enable)") : theme.fg("dim", " Include (enable)"),
800
+ this.globAction==="exclude" ? theme.fg("warning", "▶ Exclude (disable)") : theme.fg("dim", " Exclude (disable)"),
801
+ theme.fg("dim", "Tab switch · i/e shortcut · Enter apply · Esc cancel"),
802
+ ];
803
+ }
804
+ ```
805
+
806
+ Polish with `Container` border for v2.
807
+
808
+ - [ ] **Step 5: Manual test**
809
+
810
+ `/modelconf` → `x` → type `luna` → see count `2` → `Tab` to `Include` → `Enter` → verify those rows now `◉` visible; press `x` → `luna` → `Exclude` → rows become `○`; `Enter` to save → check settings.
811
+
812
+ - [ ] **Step 6: Commit**
813
+
814
+ ```
815
+ feat(modelconf): bulk glob menu on x for include/exclude by keyword
816
+ ```
817
+
818
+ ---
819
+
820
+ ### Task 9: Polish, key legend, empty states, persistence edge cases & docs
821
+
822
+ **Files:**
823
+ - Modify: `./src/ui/ModelConfView.ts` (legend, empty state, scroll window, provider collapse via Enter? optional)
824
+ - Modify: `./README.md`
825
+ - Modify: `./index.ts` (keybinding `Ctrl+Alt+M` optional shortcut)
826
+ - Test: full manual pass + `npx tsc --noEmit`
827
+
828
+ **Interfaces:**
829
+ - Consumes: all previous
830
+ - Produces: shippable extension
831
+
832
+ - [ ] **Step 1: Add empty state when filter yields 0**
833
+
834
+ In `render`, if `rowsToShow.length === 0`, return `[theme.fg("warning", "No models match query"), theme.fg("dim","Press Esc to clear search")]`.
835
+
836
+ - [ ] **Step 2: Clamp scroll window to terminal height**
837
+
838
+ Compute `visibleHeight = Math.min(rowsToShow.length + providerHeaders, tui.height ?? 30) - 4`. Slice window around `cursorIndex`.
839
+
840
+ - [ ] **Step 3: Add keyboard shortcut registration (optional)**
841
+
842
+ In `index.ts`, add:
843
+ ```typescript
844
+ pi.registerShortcut({ key: "m", ctrl: true, alt: true } as any, {
845
+ description: "Open modelconf",
846
+ handler: async (ctx) => { /* trigger same as /modelconf command */ }
847
+ });
848
+ ```
849
+ Guard: if `Key` API differs, skip — doc shows `registerShortcut(KeyId)`; verify at impl time.
850
+
851
+ - [ ] **Step 4: Handle edge cases**
852
+
853
+ - `enabledModels` contains stale ids for removed models → ignore in `isVisible`, but keep in file until user re-saves (filter to `allIds` intersection on save).
854
+ - Save writes `undefined` (delete key) when all visible to keep file clean; writes explicit array when filtered.
855
+ - File missing → treat as `undefined` (all visible).
856
+ - Parse error → notify `settings.json invalid JSON` and treat as `undefined` without overwriting.
857
+ - Large catalogs (2000+ models from `models-store.json`) → ensure `fuzzyFilter` stays fast (limit to 200 results shown, scroll paginated).
858
+
859
+ - [ ] **Step 5: Write README.md**
860
+
861
+ ```markdown
862
+ # modelconf
863
+
864
+ Per-provider model visibility manager.
865
+
866
+ ## Usage
867
+ /modelconf
868
+ - ↑/↓ or j/k navigate, / to fuzzy search, space toggle, x bulk, enter save, esc cancel.
869
+
870
+ ## Persistence
871
+ Writes `enabledModels` in `~/.pi/agent/settings.json`. After save, run `/reload` if models list not refreshed.
872
+ ```
873
+
874
+ - [ ] **Step 6: Final checks**
875
+
876
+ Run:
877
+ ```bash
878
+ npx tsc --noEmit --project ~/.pi/agent/extensions/modelconf/tsconfig.json
879
+ pi --help # ensure extension loads without error
880
+ ```
881
+ Manual: `/modelconf` → fuzzy `mini` → toggle → `x` → `*free*` exclude → `Enter` → `/reload` → `/model` picker confirms hidden.
882
+
883
+ - [ ] **Step 7: Commit**
884
+
885
+ ```
886
+ docs(modelconf): final polish and README
887
+ ```
888
+
889
+ ---
890
+
891
+ ## Verification Checklist
892
+
893
+ - [ ] `pi` starts, `/reload` shows no load errors for `modelconf`.
894
+ - [ ] `/modelconf` opens grouped list, provider headers with `visible/total`.
895
+ - [ ] `/` opens search input (IME cursor visible), typing filters list via fuzzy, `Esc` clears.
896
+ - [ ] `Space` flips `◉`/`○` and marks dirty (`● unsaved`), `Enter` writes `settings.json` and notifies, `Esc` with dirty prompts discard.
897
+ - [ ] `x` opens glob modal, typing `luna` matches substring, `Tab`/`i`/`e` switches include/exclude, `Enter` bulk-toggles draft, preview count correct.
898
+ - [ ] Save with all visible writes `enabledModels: undefined` (key removed); save with some hidden writes exact array; reload and reopen confirms persistence.
899
+ - [ ] Large provider (`opencode` ~ thousands in `models-store.json`) scrolls without overflow.
900
+ - [ ] `npx tsc --noEmit` passes.
901
+
902
+ ## Risks & Mitigations
903
+
904
+ - **No public `settingsManager` API in ExtensionAPI** — mitigated by direct `fs` read/write to `~/.pi/agent/settings.json` + documented fallback; spike in Task 2 verifies path and `ctx as any` alternative.
905
+ - **IME cursor not showing** — ensure `Container implements Focusable` propagates `focused` to `Input` per `docs/tui.md`; test with CJK input.
906
+ - **Performance on 3000+ models** — fuzzyFilter is O(n*q) but n=~11000 max (from `models-store.json`); limit render window and debounce input if needed.
907
+ - **Extension not auto-discovered** — must be `~/.pi/agent/extensions/modelconf/index.ts` and `settings.json` `extensions` includes `"extensions/"`; verify via `/reload` logs.
908
+
909
+ ## Out of Scope (v2)
910
+
911
+ - Provider-level collapse/expand toggle (could be `Enter` on header).
912
+ - Regex mode for glob menu.
913
+ - Project-local `.pi/settings.json` enabledModels merging.
914
+ - Undo history / diff preview before save.
915
+