@asterxsk/kiln 0.1.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 (491) hide show
  1. package/README.md +155 -170
  2. package/agent/AGENTS.md +67 -67
  3. package/agent/README.md +5 -5
  4. package/agent/extensions/ask-user/index.ts +418 -418
  5. package/agent/extensions/ask-user/install.ps1 +4 -0
  6. package/agent/extensions/ask-user/install.sh +3 -0
  7. package/agent/extensions/ask-user/package-lock.json +769 -769
  8. package/agent/extensions/ask-user/package.json +19 -19
  9. package/agent/extensions/ask-user/prompt.ts +45 -45
  10. package/agent/extensions/ask-user/tsconfig.json +7 -7
  11. package/agent/extensions/background-terminals/docs/implementation-guide.md +942 -942
  12. package/agent/extensions/background-terminals/index.ts +627 -627
  13. package/agent/extensions/background-terminals/install.ps1 +4 -0
  14. package/agent/extensions/background-terminals/install.sh +3 -0
  15. package/agent/extensions/background-terminals/manager.test.ts +735 -735
  16. package/agent/extensions/background-terminals/output.test.ts +109 -109
  17. package/agent/extensions/background-terminals/package-lock.json +769 -769
  18. package/agent/extensions/background-terminals/package.json +17 -17
  19. package/agent/extensions/background-terminals/prompt.test.ts +125 -125
  20. package/agent/extensions/background-terminals/ps.test.ts +82 -82
  21. package/agent/extensions/background-terminals/result-delivery.test.ts +44 -44
  22. package/agent/extensions/background-terminals/src/domain.ts +87 -87
  23. package/agent/extensions/background-terminals/src/manager.ts +907 -907
  24. package/agent/extensions/background-terminals/src/output.ts +84 -84
  25. package/agent/extensions/background-terminals/src/prompt.ts +142 -142
  26. package/agent/extensions/background-terminals/src/result-delivery.ts +27 -27
  27. package/agent/extensions/background-terminals/src/runtime.ts +36 -36
  28. package/agent/extensions/background-terminals/src/ui/output-view.ts +79 -79
  29. package/agent/extensions/background-terminals/src/ui/ps.ts +621 -621
  30. package/agent/extensions/background-terminals/tsconfig.json +7 -7
  31. package/agent/extensions/destructive/install.ps1 +4 -0
  32. package/agent/extensions/destructive/install.sh +3 -0
  33. package/agent/extensions/file-search/index.spec.ts +443 -443
  34. package/agent/extensions/file-search/index.ts +459 -459
  35. package/agent/extensions/file-search/install.ps1 +4 -0
  36. package/agent/extensions/file-search/install.sh +3 -0
  37. package/agent/extensions/file-search/package-lock.json +2253 -2253
  38. package/agent/extensions/file-search/package.json +23 -23
  39. package/agent/extensions/file-search/src/args.ts +122 -122
  40. package/agent/extensions/file-search/src/binaries.ts +422 -422
  41. package/agent/extensions/file-search/src/output.ts +126 -126
  42. package/agent/extensions/file-search/src/process.ts +146 -146
  43. package/agent/extensions/file-search/src/prompt.ts +52 -52
  44. package/agent/extensions/file-search/tsconfig.json +7 -7
  45. package/agent/extensions/goal/install.ps1 +4 -0
  46. package/agent/extensions/goal/install.sh +3 -0
  47. package/agent/extensions/herdr-agent-state.ts +257 -0
  48. package/agent/extensions/kiln-update/README.md +25 -0
  49. package/agent/extensions/kiln-update/index.ts +99 -0
  50. package/agent/extensions/kiln-update/install.ps1 +27 -0
  51. package/agent/extensions/kiln-update/install.sh +24 -0
  52. package/agent/extensions/modelconf/PLAN.md +915 -915
  53. package/agent/extensions/modelconf/index.ts +296 -296
  54. package/agent/extensions/modelconf/install.ps1 +4 -0
  55. package/agent/extensions/modelconf/install.sh +3 -0
  56. package/agent/extensions/modelconf/src/ui/ModelConfView.ts +1101 -1101
  57. package/agent/extensions/pi-web-access/CHANGELOG.md +690 -690
  58. package/agent/extensions/pi-web-access/LICENSE +21 -21
  59. package/agent/extensions/pi-web-access/README.md +470 -470
  60. package/agent/extensions/pi-web-access/SECURITY.md +5 -5
  61. package/agent/extensions/pi-web-access/activity.ts +101 -101
  62. package/agent/extensions/pi-web-access/auth-fetch.ts +148 -148
  63. package/agent/extensions/pi-web-access/brightdata-unlocker.ts +272 -272
  64. package/agent/extensions/pi-web-access/chrome-cookies.ts +669 -669
  65. package/agent/extensions/pi-web-access/content-find.ts +139 -139
  66. package/agent/extensions/pi-web-access/credential-source.ts +191 -191
  67. package/agent/extensions/pi-web-access/data-uri-sanitize.ts +406 -406
  68. package/agent/extensions/pi-web-access/datalab-pdf-extract.ts +568 -568
  69. package/agent/extensions/pi-web-access/declared-web-links.ts +173 -173
  70. package/agent/extensions/pi-web-access/evidence/CONTRACT-EVIDENCE.md +496 -496
  71. package/agent/extensions/pi-web-access/evidence/contract-probe.mjs +140 -140
  72. package/agent/extensions/pi-web-access/exa.ts +526 -526
  73. package/agent/extensions/pi-web-access/extract.ts +1196 -1196
  74. package/agent/extensions/pi-web-access/feature-config.ts +29 -29
  75. package/agent/extensions/pi-web-access/fetch-params.ts +111 -111
  76. package/agent/extensions/pi-web-access/gemini-adc.ts +298 -298
  77. package/agent/extensions/pi-web-access/gemini-api.ts +353 -353
  78. package/agent/extensions/pi-web-access/gemini-pdf-extract.ts +108 -108
  79. package/agent/extensions/pi-web-access/gemini-url-context.ts +128 -128
  80. package/agent/extensions/pi-web-access/gemini-web-config.ts +101 -101
  81. package/agent/extensions/pi-web-access/gemini-web.ts +487 -487
  82. package/agent/extensions/pi-web-access/github-api.ts +197 -197
  83. package/agent/extensions/pi-web-access/github-extract.ts +746 -746
  84. package/agent/extensions/pi-web-access/github-issue-pr.ts +700 -700
  85. package/agent/extensions/pi-web-access/index.ts +1737 -1737
  86. package/agent/extensions/pi-web-access/package-lock.json +5808 -5808
  87. package/agent/extensions/pi-web-access/package.json +64 -64
  88. package/agent/extensions/pi-web-access/page-query.ts +96 -96
  89. package/agent/extensions/pi-web-access/pdf-extract.ts +409 -409
  90. package/agent/extensions/pi-web-access/promise-try.d.ts +7 -7
  91. package/agent/extensions/pi-web-access/query-rewrite.ts +51 -51
  92. package/agent/extensions/pi-web-access/render-search-error.ts +170 -170
  93. package/agent/extensions/pi-web-access/rsc-extract.ts +338 -338
  94. package/agent/extensions/pi-web-access/source-check.ts +282 -282
  95. package/agent/extensions/pi-web-access/ssrf-protection.ts +526 -526
  96. package/agent/extensions/pi-web-access/storage.ts +521 -521
  97. package/agent/extensions/pi-web-access/summary-model-scope.ts +125 -125
  98. package/agent/extensions/pi-web-access/test/auth-fetch.test.mjs +208 -208
  99. package/agent/extensions/pi-web-access/test/brightdata-unlocker.test.mjs +840 -840
  100. package/agent/extensions/pi-web-access/test/chrome-cookie-extraction.test.mjs +441 -441
  101. package/agent/extensions/pi-web-access/test/config-path.test.mjs +283 -283
  102. package/agent/extensions/pi-web-access/test/content-find.test.mjs +25 -25
  103. package/agent/extensions/pi-web-access/test/credential-source.test.mjs +118 -118
  104. package/agent/extensions/pi-web-access/test/data-uri-sanitize.test.mjs +210 -210
  105. package/agent/extensions/pi-web-access/test/datalab-pdf-extract.test.mjs +552 -552
  106. package/agent/extensions/pi-web-access/test/declared-web-links.test.mjs +212 -212
  107. package/agent/extensions/pi-web-access/test/fetch-answer-storage.test.mjs +40 -40
  108. package/agent/extensions/pi-web-access/test/fetch-cache-storage.test.mjs +334 -334
  109. package/agent/extensions/pi-web-access/test/fetch-content-domain-policy.test.mjs +95 -95
  110. package/agent/extensions/pi-web-access/test/fetch-modes.test.mjs +53 -53
  111. package/agent/extensions/pi-web-access/test/fetch-not-found-guidance.test.mjs +92 -92
  112. package/agent/extensions/pi-web-access/test/fetch-params.test.mjs +86 -86
  113. package/agent/extensions/pi-web-access/test/fetch-render-call.test.mjs +34 -34
  114. package/agent/extensions/pi-web-access/test/fetch-routing.test.mjs +173 -173
  115. package/agent/extensions/pi-web-access/test/gemini-adc-auth.test.mjs +257 -257
  116. package/agent/extensions/pi-web-access/test/gemini-api-transport.test.mjs +170 -170
  117. package/agent/extensions/pi-web-access/test/gemini-pdf-extract.test.mjs +133 -133
  118. package/agent/extensions/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +178 -178
  119. package/agent/extensions/pi-web-access/test/gemini-web-header-overflow.test.mjs +148 -148
  120. package/agent/extensions/pi-web-access/test/get-search-content.test.mjs +223 -223
  121. package/agent/extensions/pi-web-access/test/github-extract.test.mjs +378 -378
  122. package/agent/extensions/pi-web-access/test/github-issue-pr.test.mjs +565 -565
  123. package/agent/extensions/pi-web-access/test/inline-content-config.test.mjs +99 -99
  124. package/agent/extensions/pi-web-access/test/lazy-extract-load.test.mjs +118 -118
  125. package/agent/extensions/pi-web-access/test/local-video-oversize.test.mjs +52 -52
  126. package/agent/extensions/pi-web-access/test/package-typebox-dependency.test.mjs +50 -50
  127. package/agent/extensions/pi-web-access/test/page-query.test.mjs +51 -51
  128. package/agent/extensions/pi-web-access/test/pdf-config.test.mjs +140 -140
  129. package/agent/extensions/pi-web-access/test/pdf-extract.test.mjs +500 -500
  130. package/agent/extensions/pi-web-access/test/proxy-transport.test.mjs +286 -286
  131. package/agent/extensions/pi-web-access/test/query-rewrite.test.mjs +52 -52
  132. package/agent/extensions/pi-web-access/test/rsc-fallback.test.mjs +102 -102
  133. package/agent/extensions/pi-web-access/test/search-error-render.test.mjs +152 -152
  134. package/agent/extensions/pi-web-access/test/search-providers.test.mjs +274 -274
  135. package/agent/extensions/pi-web-access/test/source-check.test.mjs +179 -179
  136. package/agent/extensions/pi-web-access/test/ssrf-allow-ranges-config.test.mjs +205 -205
  137. package/agent/extensions/pi-web-access/test/ssrf-protection.test.mjs +456 -456
  138. package/agent/extensions/pi-web-access/test/tool-registration-config.test.mjs +182 -182
  139. package/agent/extensions/pi-web-access/test/youtube-extract-errors.test.mjs +64 -64
  140. package/agent/extensions/pi-web-access/tsconfig.json +11 -11
  141. package/agent/extensions/pi-web-access/utils.ts +451 -451
  142. package/agent/extensions/pi-web-access/video-extract.ts +392 -392
  143. package/agent/extensions/pi-web-access/youtube-extract.ts +328 -328
  144. package/agent/extensions/shared/activity-status.ts +31 -31
  145. package/agent/extensions/shared/child-session.test.ts +270 -270
  146. package/agent/extensions/shared/child-session.ts +148 -148
  147. package/agent/extensions/shared/context-utilization.test.ts +48 -48
  148. package/agent/extensions/shared/context-utilization.ts +47 -47
  149. package/agent/extensions/shared/dashboard-state.ts +99 -99
  150. package/agent/extensions/shared/install.ps1 +4 -0
  151. package/agent/extensions/shared/install.sh +3 -0
  152. package/agent/extensions/shared/tool-call-timeout.test.ts +117 -117
  153. package/agent/extensions/shared/tool-call-timeout.ts +104 -104
  154. package/agent/extensions/skillsconf/install.ps1 +4 -0
  155. package/agent/extensions/skillsconf/install.sh +3 -0
  156. package/agent/extensions/statusline/index.ts +282 -0
  157. package/agent/extensions/statusline/install.ps1 +27 -0
  158. package/agent/extensions/statusline/install.sh +24 -0
  159. package/agent/extensions/subagents/by-the-way.test.ts +29 -29
  160. package/agent/extensions/subagents/claude.test.ts +119 -119
  161. package/agent/extensions/subagents/codex.test.ts +102 -102
  162. package/agent/extensions/subagents/context-usage.test.ts +107 -107
  163. package/agent/extensions/subagents/docs/design-plan.md +568 -568
  164. package/agent/extensions/subagents/docs/effect-v4-extension-guide.md +354 -354
  165. package/agent/extensions/subagents/docs/effect-v4-notes.md +571 -571
  166. package/agent/extensions/subagents/index.ts +779 -779
  167. package/agent/extensions/subagents/install.ps1 +4 -0
  168. package/agent/extensions/subagents/install.sh +3 -0
  169. package/agent/extensions/subagents/manager.test.ts +276 -276
  170. package/agent/extensions/subagents/package-lock.json +2244 -2244
  171. package/agent/extensions/subagents/package.json +19 -19
  172. package/agent/extensions/subagents/result-delivery.test.ts +27 -27
  173. package/agent/extensions/subagents/src/backend.ts +73 -73
  174. package/agent/extensions/subagents/src/backends/claude.ts +701 -701
  175. package/agent/extensions/subagents/src/backends/codex.ts +1060 -1060
  176. package/agent/extensions/subagents/src/backends/pi.ts +575 -575
  177. package/agent/extensions/subagents/src/backends/stub.ts +300 -300
  178. package/agent/extensions/subagents/src/by-the-way.ts +21 -21
  179. package/agent/extensions/subagents/src/domain.ts +253 -253
  180. package/agent/extensions/subagents/src/format.ts +74 -74
  181. package/agent/extensions/subagents/src/manager.ts +736 -736
  182. package/agent/extensions/subagents/src/prompt.ts +92 -92
  183. package/agent/extensions/subagents/src/result-delivery.ts +20 -20
  184. package/agent/extensions/subagents/src/runtime.ts +53 -53
  185. package/agent/extensions/subagents/src/ui/takeover.ts +583 -583
  186. package/agent/extensions/subagents/src/ui/transcript.ts +201 -201
  187. package/agent/extensions/subagents/takeover.test.ts +29 -29
  188. package/agent/extensions/subagents/tsconfig.json +7 -7
  189. package/agent/extensions/taste/install.ps1 +4 -0
  190. package/agent/extensions/taste/install.sh +3 -0
  191. package/agent/extensions/todo/AGENTS.md +38 -38
  192. package/agent/extensions/todo/LICENSE +21 -21
  193. package/agent/extensions/todo/config.ts +55 -55
  194. package/agent/extensions/todo/index.ts +151 -151
  195. package/agent/extensions/todo/install.ps1 +4 -0
  196. package/agent/extensions/todo/install.sh +3 -0
  197. package/agent/extensions/todo/locales/de.json +17 -17
  198. package/agent/extensions/todo/locales/en.json +15 -15
  199. package/agent/extensions/todo/locales/es.json +17 -17
  200. package/agent/extensions/todo/locales/fr.json +17 -17
  201. package/agent/extensions/todo/locales/pt-BR.json +17 -17
  202. package/agent/extensions/todo/locales/pt.json +17 -17
  203. package/agent/extensions/todo/locales/ru.json +17 -17
  204. package/agent/extensions/todo/locales/uk.json +17 -17
  205. package/agent/extensions/todo/locales/zh.json +17 -17
  206. package/agent/extensions/todo/package-lock.json +3358 -3358
  207. package/agent/extensions/todo/package.json +67 -67
  208. package/agent/extensions/todo/state/i18n-bridge.ts +64 -64
  209. package/agent/extensions/todo/state/invariants.ts +20 -20
  210. package/agent/extensions/todo/state/replay.ts +38 -38
  211. package/agent/extensions/todo/state/selectors.ts +107 -107
  212. package/agent/extensions/todo/state/state-reducer.ts +326 -326
  213. package/agent/extensions/todo/state/state.ts +18 -18
  214. package/agent/extensions/todo/state/store.ts +82 -82
  215. package/agent/extensions/todo/state/task-graph.ts +57 -57
  216. package/agent/extensions/todo/todo-overlay.ts +200 -200
  217. package/agent/extensions/todo/todo.ts +155 -155
  218. package/agent/extensions/todo/tool/response-envelope.ts +109 -109
  219. package/agent/extensions/todo/tool/types.ts +206 -206
  220. package/agent/extensions/todo/view/format.ts +177 -177
  221. package/agent/extensions/trim-context/install.ps1 +4 -0
  222. package/agent/extensions/trim-context/install.sh +3 -0
  223. package/agent/keybindings.json +7 -7
  224. package/agent/version.txt +1 -0
  225. package/agent.bak.20260905-190020/AGENTS.md +67 -0
  226. package/agent.bak.20260905-190020/README.md +5 -0
  227. package/agent.bak.20260905-190020/extensions/ask-user/index.ts +418 -0
  228. package/agent.bak.20260905-190020/extensions/ask-user/package-lock.json +769 -0
  229. package/agent.bak.20260905-190020/extensions/ask-user/package.json +19 -0
  230. package/agent.bak.20260905-190020/extensions/ask-user/prompt.ts +45 -0
  231. package/agent.bak.20260905-190020/extensions/ask-user/tsconfig.json +7 -0
  232. package/agent.bak.20260905-190020/extensions/background-terminals/docs/implementation-guide.md +942 -0
  233. package/agent.bak.20260905-190020/extensions/background-terminals/index.ts +629 -0
  234. package/agent.bak.20260905-190020/extensions/background-terminals/install.ps1 +23 -0
  235. package/agent.bak.20260905-190020/extensions/background-terminals/install.sh +21 -0
  236. package/agent.bak.20260905-190020/extensions/background-terminals/package-lock.json +769 -0
  237. package/agent.bak.20260905-190020/extensions/background-terminals/package.json +17 -0
  238. package/agent.bak.20260905-190020/extensions/background-terminals/src/domain.ts +87 -0
  239. package/agent.bak.20260905-190020/extensions/background-terminals/src/manager.ts +907 -0
  240. package/agent.bak.20260905-190020/extensions/background-terminals/src/output.ts +84 -0
  241. package/agent.bak.20260905-190020/extensions/background-terminals/src/prompt.ts +142 -0
  242. package/agent.bak.20260905-190020/extensions/background-terminals/src/result-delivery.ts +27 -0
  243. package/agent.bak.20260905-190020/extensions/background-terminals/src/runtime.ts +36 -0
  244. package/agent.bak.20260905-190020/extensions/background-terminals/src/ui/output-view.ts +79 -0
  245. package/agent.bak.20260905-190020/extensions/background-terminals/src/ui/ps.ts +621 -0
  246. package/agent.bak.20260905-190020/extensions/background-terminals/src/widget-order.ts +121 -0
  247. package/agent.bak.20260905-190020/extensions/background-terminals/test/manager.test.ts +735 -0
  248. package/agent.bak.20260905-190020/extensions/background-terminals/test/output.test.ts +109 -0
  249. package/agent.bak.20260905-190020/extensions/background-terminals/test/prompt.test.ts +125 -0
  250. package/agent.bak.20260905-190020/extensions/background-terminals/test/ps.test.ts +82 -0
  251. package/agent.bak.20260905-190020/extensions/background-terminals/test/result-delivery.test.ts +44 -0
  252. package/agent.bak.20260905-190020/extensions/background-terminals/tsconfig.json +7 -0
  253. package/agent.bak.20260905-190020/extensions/destructive/README.md +34 -0
  254. package/agent.bak.20260905-190020/extensions/destructive/index.ts +124 -0
  255. package/agent.bak.20260905-190020/extensions/destructive/install.ps1 +23 -0
  256. package/agent.bak.20260905-190020/extensions/destructive/install.sh +21 -0
  257. package/agent.bak.20260905-190020/extensions/file-search/index.ts +459 -0
  258. package/agent.bak.20260905-190020/extensions/file-search/install.ps1 +23 -0
  259. package/agent.bak.20260905-190020/extensions/file-search/install.sh +21 -0
  260. package/agent.bak.20260905-190020/extensions/file-search/package-lock.json +2253 -0
  261. package/agent.bak.20260905-190020/extensions/file-search/package.json +23 -0
  262. package/agent.bak.20260905-190020/extensions/file-search/src/args.ts +122 -0
  263. package/agent.bak.20260905-190020/extensions/file-search/src/binaries.ts +422 -0
  264. package/agent.bak.20260905-190020/extensions/file-search/src/output.ts +126 -0
  265. package/agent.bak.20260905-190020/extensions/file-search/src/process.ts +146 -0
  266. package/agent.bak.20260905-190020/extensions/file-search/src/prompt.ts +52 -0
  267. package/agent.bak.20260905-190020/extensions/file-search/test/index.spec.ts +443 -0
  268. package/agent.bak.20260905-190020/extensions/file-search/tsconfig.json +7 -0
  269. package/agent.bak.20260905-190020/extensions/goal/README.md +50 -0
  270. package/agent.bak.20260905-190020/extensions/goal/index.ts +155 -0
  271. package/agent.bak.20260905-190020/extensions/goal/install.ps1 +23 -0
  272. package/agent.bak.20260905-190020/extensions/goal/install.sh +21 -0
  273. package/agent.bak.20260905-190020/extensions/kiln-update/README.md +25 -0
  274. package/agent.bak.20260905-190020/extensions/kiln-update/index.ts +99 -0
  275. package/agent.bak.20260905-190020/extensions/kiln-update/install.ps1 +23 -0
  276. package/agent.bak.20260905-190020/extensions/kiln-update/install.sh +21 -0
  277. package/agent.bak.20260905-190020/extensions/modelconf/PLAN.md +915 -0
  278. package/agent.bak.20260905-190020/extensions/modelconf/README.md +66 -0
  279. package/agent.bak.20260905-190020/extensions/modelconf/index.ts +296 -0
  280. package/agent.bak.20260905-190020/extensions/modelconf/install.ps1 +23 -0
  281. package/agent.bak.20260905-190020/extensions/modelconf/install.sh +21 -0
  282. package/agent.bak.20260905-190020/extensions/modelconf/package-lock.json +1809 -0
  283. package/agent.bak.20260905-190020/extensions/modelconf/package.json +13 -0
  284. package/agent.bak.20260905-190020/extensions/modelconf/src/fuzzy.ts +26 -0
  285. package/agent.bak.20260905-190020/extensions/modelconf/src/glob.ts +13 -0
  286. package/agent.bak.20260905-190020/extensions/modelconf/src/persistence.test.ts +46 -0
  287. package/agent.bak.20260905-190020/extensions/modelconf/src/persistence.ts +164 -0
  288. package/agent.bak.20260905-190020/extensions/modelconf/src/ui/ModelConfView.test.ts +75 -0
  289. package/agent.bak.20260905-190020/extensions/modelconf/src/ui/ModelConfView.ts +1101 -0
  290. package/agent.bak.20260905-190020/extensions/modelconf/tsconfig.json +15 -0
  291. package/agent.bak.20260905-190020/extensions/pi-web-access/CHANGELOG.md +690 -0
  292. package/agent.bak.20260905-190020/extensions/pi-web-access/LICENSE +21 -0
  293. package/agent.bak.20260905-190020/extensions/pi-web-access/README.md +470 -0
  294. package/agent.bak.20260905-190020/extensions/pi-web-access/SECURITY.md +5 -0
  295. package/agent.bak.20260905-190020/extensions/pi-web-access/activity.ts +101 -0
  296. package/agent.bak.20260905-190020/extensions/pi-web-access/auth-fetch.ts +148 -0
  297. package/agent.bak.20260905-190020/extensions/pi-web-access/banner.png +0 -0
  298. package/agent.bak.20260905-190020/extensions/pi-web-access/brightdata-unlocker.ts +272 -0
  299. package/agent.bak.20260905-190020/extensions/pi-web-access/chrome-cookies.ts +669 -0
  300. package/agent.bak.20260905-190020/extensions/pi-web-access/content-find.ts +139 -0
  301. package/agent.bak.20260905-190020/extensions/pi-web-access/credential-source.ts +191 -0
  302. package/agent.bak.20260905-190020/extensions/pi-web-access/data-uri-sanitize.ts +406 -0
  303. package/agent.bak.20260905-190020/extensions/pi-web-access/datalab-pdf-extract.ts +568 -0
  304. package/agent.bak.20260905-190020/extensions/pi-web-access/declared-web-links.ts +173 -0
  305. package/agent.bak.20260905-190020/extensions/pi-web-access/evidence/CONTRACT-EVIDENCE.md +496 -0
  306. package/agent.bak.20260905-190020/extensions/pi-web-access/evidence/contract-probe.mjs +140 -0
  307. package/agent.bak.20260905-190020/extensions/pi-web-access/exa.ts +526 -0
  308. package/agent.bak.20260905-190020/extensions/pi-web-access/extract.ts +1196 -0
  309. package/agent.bak.20260905-190020/extensions/pi-web-access/feature-config.ts +29 -0
  310. package/agent.bak.20260905-190020/extensions/pi-web-access/fetch-params.ts +111 -0
  311. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-adc.ts +298 -0
  312. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-api.ts +353 -0
  313. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-pdf-extract.ts +108 -0
  314. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-search.ts +21 -0
  315. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-url-context.ts +128 -0
  316. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-web-config.ts +101 -0
  317. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-web.ts +487 -0
  318. package/agent.bak.20260905-190020/extensions/pi-web-access/github-api.ts +197 -0
  319. package/agent.bak.20260905-190020/extensions/pi-web-access/github-extract.ts +746 -0
  320. package/agent.bak.20260905-190020/extensions/pi-web-access/github-issue-pr.ts +700 -0
  321. package/agent.bak.20260905-190020/extensions/pi-web-access/index.ts +1737 -0
  322. package/agent.bak.20260905-190020/extensions/pi-web-access/package-lock.json +5808 -0
  323. package/agent.bak.20260905-190020/extensions/pi-web-access/package.json +64 -0
  324. package/agent.bak.20260905-190020/extensions/pi-web-access/page-query.ts +96 -0
  325. package/agent.bak.20260905-190020/extensions/pi-web-access/pdf-extract.ts +409 -0
  326. package/agent.bak.20260905-190020/extensions/pi-web-access/pi-web-fetch-demo.mp4 +0 -0
  327. package/agent.bak.20260905-190020/extensions/pi-web-access/promise-try.d.ts +7 -0
  328. package/agent.bak.20260905-190020/extensions/pi-web-access/query-rewrite.ts +51 -0
  329. package/agent.bak.20260905-190020/extensions/pi-web-access/render-search-error.ts +170 -0
  330. package/agent.bak.20260905-190020/extensions/pi-web-access/rsc-extract.ts +338 -0
  331. package/agent.bak.20260905-190020/extensions/pi-web-access/search-types.ts +20 -0
  332. package/agent.bak.20260905-190020/extensions/pi-web-access/source-check.ts +282 -0
  333. package/agent.bak.20260905-190020/extensions/pi-web-access/ssrf-protection.ts +526 -0
  334. package/agent.bak.20260905-190020/extensions/pi-web-access/storage.ts +521 -0
  335. package/agent.bak.20260905-190020/extensions/pi-web-access/summary-model-scope.ts +125 -0
  336. package/agent.bak.20260905-190020/extensions/pi-web-access/test/auth-fetch.test.mjs +208 -0
  337. package/agent.bak.20260905-190020/extensions/pi-web-access/test/brightdata-unlocker.test.mjs +840 -0
  338. package/agent.bak.20260905-190020/extensions/pi-web-access/test/chrome-cookie-extraction.test.mjs +441 -0
  339. package/agent.bak.20260905-190020/extensions/pi-web-access/test/config-path.test.mjs +283 -0
  340. package/agent.bak.20260905-190020/extensions/pi-web-access/test/content-find.test.mjs +25 -0
  341. package/agent.bak.20260905-190020/extensions/pi-web-access/test/credential-source.test.mjs +118 -0
  342. package/agent.bak.20260905-190020/extensions/pi-web-access/test/data-uri-sanitize.test.mjs +210 -0
  343. package/agent.bak.20260905-190020/extensions/pi-web-access/test/datalab-pdf-extract.test.mjs +552 -0
  344. package/agent.bak.20260905-190020/extensions/pi-web-access/test/declared-web-links.test.mjs +212 -0
  345. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-answer-storage.test.mjs +40 -0
  346. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-cache-storage.test.mjs +334 -0
  347. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-content-domain-policy.test.mjs +95 -0
  348. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-modes.test.mjs +53 -0
  349. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-not-found-guidance.test.mjs +92 -0
  350. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-params.test.mjs +86 -0
  351. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-render-call.test.mjs +34 -0
  352. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-routing.test.mjs +173 -0
  353. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-adc-auth.test.mjs +257 -0
  354. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-api-transport.test.mjs +170 -0
  355. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-pdf-extract.test.mjs +133 -0
  356. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +178 -0
  357. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-web-header-overflow.test.mjs +148 -0
  358. package/agent.bak.20260905-190020/extensions/pi-web-access/test/get-search-content.test.mjs +223 -0
  359. package/agent.bak.20260905-190020/extensions/pi-web-access/test/github-extract.test.mjs +378 -0
  360. package/agent.bak.20260905-190020/extensions/pi-web-access/test/github-issue-pr.test.mjs +565 -0
  361. package/agent.bak.20260905-190020/extensions/pi-web-access/test/inline-content-config.test.mjs +99 -0
  362. package/agent.bak.20260905-190020/extensions/pi-web-access/test/lazy-extract-load.test.mjs +118 -0
  363. package/agent.bak.20260905-190020/extensions/pi-web-access/test/local-video-oversize.test.mjs +52 -0
  364. package/agent.bak.20260905-190020/extensions/pi-web-access/test/package-typebox-dependency.test.mjs +50 -0
  365. package/agent.bak.20260905-190020/extensions/pi-web-access/test/page-query.test.mjs +51 -0
  366. package/agent.bak.20260905-190020/extensions/pi-web-access/test/pdf-config.test.mjs +140 -0
  367. package/agent.bak.20260905-190020/extensions/pi-web-access/test/pdf-extract.test.mjs +500 -0
  368. package/agent.bak.20260905-190020/extensions/pi-web-access/test/proxy-transport.test.mjs +286 -0
  369. package/agent.bak.20260905-190020/extensions/pi-web-access/test/query-rewrite.test.mjs +52 -0
  370. package/agent.bak.20260905-190020/extensions/pi-web-access/test/rsc-fallback.test.mjs +102 -0
  371. package/agent.bak.20260905-190020/extensions/pi-web-access/test/search-error-render.test.mjs +152 -0
  372. package/agent.bak.20260905-190020/extensions/pi-web-access/test/search-providers.test.mjs +274 -0
  373. package/agent.bak.20260905-190020/extensions/pi-web-access/test/source-check.test.mjs +179 -0
  374. package/agent.bak.20260905-190020/extensions/pi-web-access/test/ssrf-allow-ranges-config.test.mjs +205 -0
  375. package/agent.bak.20260905-190020/extensions/pi-web-access/test/ssrf-protection.test.mjs +456 -0
  376. package/agent.bak.20260905-190020/extensions/pi-web-access/test/summary-model-scope.test.mjs +106 -0
  377. package/agent.bak.20260905-190020/extensions/pi-web-access/test/tool-registration-config.test.mjs +182 -0
  378. package/agent.bak.20260905-190020/extensions/pi-web-access/test/web-search-answer-render.test.mjs +66 -0
  379. package/agent.bak.20260905-190020/extensions/pi-web-access/test/youtube-extract-errors.test.mjs +64 -0
  380. package/agent.bak.20260905-190020/extensions/pi-web-access/tsconfig.json +11 -0
  381. package/agent.bak.20260905-190020/extensions/pi-web-access/utils.ts +451 -0
  382. package/agent.bak.20260905-190020/extensions/pi-web-access/video-extract.ts +392 -0
  383. package/agent.bak.20260905-190020/extensions/pi-web-access/youtube-extract.ts +328 -0
  384. package/agent.bak.20260905-190020/extensions/shared/activity-status.ts +31 -0
  385. package/agent.bak.20260905-190020/extensions/shared/child-session.ts +148 -0
  386. package/agent.bak.20260905-190020/extensions/shared/context-utilization.ts +47 -0
  387. package/agent.bak.20260905-190020/extensions/shared/dashboard-state.ts +99 -0
  388. package/agent.bak.20260905-190020/extensions/shared/install.ps1 +23 -0
  389. package/agent.bak.20260905-190020/extensions/shared/install.sh +21 -0
  390. package/agent.bak.20260905-190020/extensions/shared/test/child-session.test.ts +270 -0
  391. package/agent.bak.20260905-190020/extensions/shared/test/context-utilization.test.ts +48 -0
  392. package/agent.bak.20260905-190020/extensions/shared/test/tool-call-timeout.test.ts +117 -0
  393. package/agent.bak.20260905-190020/extensions/shared/test/widget-order.test.ts +167 -0
  394. package/agent.bak.20260905-190020/extensions/shared/tool-call-timeout.ts +104 -0
  395. package/agent.bak.20260905-190020/extensions/skillsconf/README.md +74 -0
  396. package/agent.bak.20260905-190020/extensions/skillsconf/index.ts +92 -0
  397. package/agent.bak.20260905-190020/extensions/skillsconf/install.ps1 +23 -0
  398. package/agent.bak.20260905-190020/extensions/skillsconf/install.sh +21 -0
  399. package/agent.bak.20260905-190020/extensions/skillsconf/package-lock.json +1809 -0
  400. package/agent.bak.20260905-190020/extensions/skillsconf/package.json +18 -0
  401. package/agent.bak.20260905-190020/extensions/skillsconf/src/delete-skill.test.ts +64 -0
  402. package/agent.bak.20260905-190020/extensions/skillsconf/src/delete-skill.ts +45 -0
  403. package/agent.bak.20260905-190020/extensions/skillsconf/src/filter.test.ts +80 -0
  404. package/agent.bak.20260905-190020/extensions/skillsconf/src/filter.ts +74 -0
  405. package/agent.bak.20260905-190020/extensions/skillsconf/src/fuzzy.ts +26 -0
  406. package/agent.bak.20260905-190020/extensions/skillsconf/src/persistence.test.ts +72 -0
  407. package/agent.bak.20260905-190020/extensions/skillsconf/src/persistence.ts +169 -0
  408. package/agent.bak.20260905-190020/extensions/skillsconf/src/ui/SkillConfView.test.ts +337 -0
  409. package/agent.bak.20260905-190020/extensions/skillsconf/src/ui/SkillConfView.ts +758 -0
  410. package/agent.bak.20260905-190020/extensions/skillsconf/src/ui/text-input.ts +91 -0
  411. package/agent.bak.20260905-190020/extensions/skillsconf/src/ui/tui-helpers.ts +144 -0
  412. package/agent.bak.20260905-190020/extensions/skillsconf/tsconfig.json +15 -0
  413. package/agent.bak.20260905-190020/extensions/statusline/install.ps1 +23 -0
  414. package/agent.bak.20260905-190020/extensions/statusline/install.sh +21 -0
  415. package/agent.bak.20260905-190020/extensions/subagents/docs/design-plan.md +568 -0
  416. package/agent.bak.20260905-190020/extensions/subagents/docs/effect-v4-extension-guide.md +354 -0
  417. package/agent.bak.20260905-190020/extensions/subagents/docs/effect-v4-notes.md +571 -0
  418. package/agent.bak.20260905-190020/extensions/subagents/index.ts +826 -0
  419. package/agent.bak.20260905-190020/extensions/subagents/install.ps1 +23 -0
  420. package/agent.bak.20260905-190020/extensions/subagents/install.sh +21 -0
  421. package/agent.bak.20260905-190020/extensions/subagents/package-lock.json +2244 -0
  422. package/agent.bak.20260905-190020/extensions/subagents/package.json +19 -0
  423. package/agent.bak.20260905-190020/extensions/subagents/src/backend.ts +73 -0
  424. package/agent.bak.20260905-190020/extensions/subagents/src/backends/claude.ts +701 -0
  425. package/agent.bak.20260905-190020/extensions/subagents/src/backends/codex.ts +1060 -0
  426. package/agent.bak.20260905-190020/extensions/subagents/src/backends/pi.ts +575 -0
  427. package/agent.bak.20260905-190020/extensions/subagents/src/backends/stub.ts +300 -0
  428. package/agent.bak.20260905-190020/extensions/subagents/src/by-the-way.ts +21 -0
  429. package/agent.bak.20260905-190020/extensions/subagents/src/domain.ts +253 -0
  430. package/agent.bak.20260905-190020/extensions/subagents/src/format.ts +74 -0
  431. package/agent.bak.20260905-190020/extensions/subagents/src/manager.ts +736 -0
  432. package/agent.bak.20260905-190020/extensions/subagents/src/prompt.ts +92 -0
  433. package/agent.bak.20260905-190020/extensions/subagents/src/result-delivery.ts +20 -0
  434. package/agent.bak.20260905-190020/extensions/subagents/src/runtime.ts +53 -0
  435. package/agent.bak.20260905-190020/extensions/subagents/src/ui/takeover.ts +583 -0
  436. package/agent.bak.20260905-190020/extensions/subagents/src/ui/transcript.ts +201 -0
  437. package/agent.bak.20260905-190020/extensions/subagents/src/widget-order.ts +121 -0
  438. package/agent.bak.20260905-190020/extensions/subagents/test/by-the-way.test.ts +29 -0
  439. package/agent.bak.20260905-190020/extensions/subagents/test/claude.test.ts +119 -0
  440. package/agent.bak.20260905-190020/extensions/subagents/test/codex.test.ts +102 -0
  441. package/agent.bak.20260905-190020/extensions/subagents/test/context-usage.test.ts +107 -0
  442. package/agent.bak.20260905-190020/extensions/subagents/test/manager.test.ts +276 -0
  443. package/agent.bak.20260905-190020/extensions/subagents/test/result-delivery.test.ts +27 -0
  444. package/agent.bak.20260905-190020/extensions/subagents/test/takeover.test.ts +29 -0
  445. package/agent.bak.20260905-190020/extensions/subagents/tsconfig.json +7 -0
  446. package/agent.bak.20260905-190020/extensions/taste/index.ts +443 -0
  447. package/agent.bak.20260905-190020/extensions/taste/install.ps1 +23 -0
  448. package/agent.bak.20260905-190020/extensions/taste/install.sh +21 -0
  449. package/agent.bak.20260905-190020/extensions/todo/AGENTS.md +38 -0
  450. package/agent.bak.20260905-190020/extensions/todo/LICENSE +21 -0
  451. package/agent.bak.20260905-190020/extensions/todo/config.ts +55 -0
  452. package/agent.bak.20260905-190020/extensions/todo/index.ts +151 -0
  453. package/agent.bak.20260905-190020/extensions/todo/install.ps1 +23 -0
  454. package/agent.bak.20260905-190020/extensions/todo/install.sh +21 -0
  455. package/agent.bak.20260905-190020/extensions/todo/locales/de.json +17 -0
  456. package/agent.bak.20260905-190020/extensions/todo/locales/en.json +15 -0
  457. package/agent.bak.20260905-190020/extensions/todo/locales/es.json +17 -0
  458. package/agent.bak.20260905-190020/extensions/todo/locales/fr.json +17 -0
  459. package/agent.bak.20260905-190020/extensions/todo/locales/pt-BR.json +17 -0
  460. package/agent.bak.20260905-190020/extensions/todo/locales/pt.json +17 -0
  461. package/agent.bak.20260905-190020/extensions/todo/locales/ru.json +17 -0
  462. package/agent.bak.20260905-190020/extensions/todo/locales/uk.json +17 -0
  463. package/agent.bak.20260905-190020/extensions/todo/locales/zh.json +17 -0
  464. package/agent.bak.20260905-190020/extensions/todo/package-lock.json +3358 -0
  465. package/agent.bak.20260905-190020/extensions/todo/package.json +67 -0
  466. package/agent.bak.20260905-190020/extensions/todo/state/i18n-bridge.ts +64 -0
  467. package/agent.bak.20260905-190020/extensions/todo/state/invariants.ts +20 -0
  468. package/agent.bak.20260905-190020/extensions/todo/state/replay.ts +38 -0
  469. package/agent.bak.20260905-190020/extensions/todo/state/selectors.ts +107 -0
  470. package/agent.bak.20260905-190020/extensions/todo/state/state-reducer.ts +326 -0
  471. package/agent.bak.20260905-190020/extensions/todo/state/state.ts +18 -0
  472. package/agent.bak.20260905-190020/extensions/todo/state/store.ts +82 -0
  473. package/agent.bak.20260905-190020/extensions/todo/state/task-graph.ts +57 -0
  474. package/agent.bak.20260905-190020/extensions/todo/todo-overlay.ts +204 -0
  475. package/agent.bak.20260905-190020/extensions/todo/todo.ts +155 -0
  476. package/agent.bak.20260905-190020/extensions/todo/tool/response-envelope.ts +109 -0
  477. package/agent.bak.20260905-190020/extensions/todo/tool/types.ts +206 -0
  478. package/agent.bak.20260905-190020/extensions/todo/view/format.ts +177 -0
  479. package/agent.bak.20260905-190020/extensions/todo/widget-order.ts +121 -0
  480. package/agent.bak.20260905-190020/extensions/trim-context/README.md +54 -0
  481. package/agent.bak.20260905-190020/extensions/trim-context/index.ts +487 -0
  482. package/agent.bak.20260905-190020/extensions/trim-context/install.ps1 +23 -0
  483. package/agent.bak.20260905-190020/extensions/trim-context/install.sh +21 -0
  484. package/agent.bak.20260905-190020/keybindings.json +7 -0
  485. package/bin/kiln.js +588 -358
  486. package/package.json +27 -21
  487. package/agent/install.ps1 +0 -527
  488. package/agent/install.sh +0 -511
  489. /package/{agent/extensions/status line → agent.bak.20260905-190020/extensions/ask-user}/install.ps1 +0 -0
  490. /package/{agent/extensions/status line → agent.bak.20260905-190020/extensions/ask-user}/install.sh +0 -0
  491. /package/{agent/extensions/status line → agent.bak.20260905-190020/extensions/statusline}/index.ts +0 -0
@@ -1,1101 +1,1101 @@
1
- import { applyBulk, toggleInEnabled } from "../persistence.js";
2
- import type { ModelId } from "../persistence.js";
3
- import { globToRegExp } from "../glob.js";
4
- import { fuzzyFilter } from "../fuzzy.js";
5
- // Minimal Component type to avoid requiring @earendil-works/pi-tui at build time.
6
- // We mirror the pi-tui Component contract used by ctx.ui.custom.
7
- export interface Component {
8
- render(width: number): string[];
9
- handleInput?(data: string): void;
10
- invalidate(): void;
11
- }
12
-
13
- // Focusable contract per docs/tui.md — propagate focused to child Input
14
- export interface Focusable {
15
- focused: boolean;
16
- }
17
-
18
- // ---- local helpers for width/truncation (fallback when pi-tui not available) ----
19
-
20
- function stripAnsi(s: string): string {
21
- // strip CSI ...m, OSC 8 hyperlink, and APC cursor marker
22
- return s.replace(/\x1b\[[0-9;]*m/g, "").replace(/\x1b\]8;;.*?\x1b\\/g, "").replace(/\x1b_pi:c\x07/g, "");
23
- }
24
-
25
- function visibleWidthFallback(s: string): number {
26
- const stripped = stripAnsi(s);
27
- let w = 0;
28
- for (const ch of stripped) {
29
- const cp = ch.codePointAt(0) ?? 0;
30
- // crude wide char detection: CJK + emoji ranges
31
- if (cp >= 0x1100 && (
32
- cp <= 0x115F || cp === 0x2329 || cp === 0x232A ||
33
- (cp >= 0x2E80 && cp <= 0xA4CF && cp !== 0x303F) ||
34
- (cp >= 0xAC00 && cp <= 0xD7A3) ||
35
- (cp >= 0xF900 && cp <= 0xFAFF) ||
36
- (cp >= 0xFE10 && cp <= 0xFE19) ||
37
- (cp >= 0xFE30 && cp <= 0xFE6F) ||
38
- (cp >= 0xFF00 && cp <= 0xFF60) ||
39
- (cp >= 0xFFE0 && cp <= 0xFFE6)
40
- )) {
41
- w += 2;
42
- } else {
43
- w += 1;
44
- }
45
- }
46
- return w;
47
- }
48
-
49
- function extractAnsiCode(str: string, pos: number): { code: string; length: number } | null {
50
- if (str[pos] !== "\x1b") return null;
51
- const next = str[pos + 1];
52
- if (next === "[") {
53
- let i = pos + 2;
54
- while (i < str.length) {
55
- const c = str[i];
56
- if ((c >= "0" && c <= "9") || c === ";" || c === ":" || c === "?" ) { i++; continue; }
57
- // final char
58
- i++;
59
- return { code: str.slice(pos, i), length: i - pos };
60
- }
61
- return null;
62
- }
63
- if (next === "]") {
64
- const end = str.indexOf("\x1b\\", pos);
65
- if (end !== -1) return { code: str.slice(pos, end + 2), length: end + 2 - pos };
66
- const bel = str.indexOf("\x07", pos);
67
- if (bel !== -1) return { code: str.slice(pos, bel + 1), length: bel + 1 - pos };
68
- return null;
69
- }
70
- if (str.startsWith("\x1b_pi:c\x07", pos)) return { code: "\x1b_pi:c\x07", length: 7 };
71
- return null;
72
- }
73
-
74
- function truncateFallback(text: string, maxWidth: number, ellipsis = "..."): string {
75
- if (maxWidth <= 0) return "";
76
- const vw = visibleWidthFallback(text);
77
- if (vw <= maxWidth) return text;
78
- const ellW = visibleWidthFallback(ellipsis);
79
- const target = maxWidth - ellW;
80
- if (target <= 0) {
81
- // truncate ellipsis itself if needed
82
- let out = "";
83
- let w = 0;
84
- let i = 0;
85
- while (i < text.length) {
86
- const ansi = extractAnsiCode(text, i);
87
- if (ansi) { out += ansi.code; i += ansi.length; continue; }
88
- // grapheme: take one codepoint
89
- const ch = text[i];
90
- // handle surrogate pair
91
- let grapheme = ch;
92
- if (ch.charCodeAt(0) >= 0xD800 && ch.charCodeAt(0) <= 0xDBFF && i + 1 < text.length) {
93
- grapheme = text.slice(i, i + 2);
94
- }
95
- const gw = visibleWidthFallback(grapheme);
96
- if (w + gw > maxWidth) break;
97
- out += grapheme;
98
- w += gw;
99
- i += grapheme.length;
100
- }
101
- return out;
102
- }
103
- let out = "";
104
- let w = 0;
105
- let i = 0;
106
- let pendingAnsi = "";
107
- while (i < text.length) {
108
- const ansi = extractAnsiCode(text, i);
109
- if (ansi) { pendingAnsi += ansi.code; out += ansi.code; i += ansi.length; continue; }
110
- let grapheme = text[i];
111
- if (grapheme.charCodeAt(0) >= 0xD800 && grapheme.charCodeAt(0) <= 0xDBFF && i + 1 < text.length) {
112
- grapheme = text.slice(i, i + 2);
113
- }
114
- const gw = visibleWidthFallback(grapheme);
115
- if (w + gw > target) break;
116
- out += grapheme;
117
- w += gw;
118
- i += grapheme.length;
119
- }
120
- out += ellipsis;
121
- // Append pending ansi resets? naive: ensure reset at end not needed
122
- return out;
123
- }
124
-
125
- // Try to use pi-tui's truncate if available at runtime (optional). For compile-time we use fallback.
126
- let truncateToWidthFn: (text: string, maxWidth: number, ellipsis?: string, pad?: boolean) => string = truncateFallback;
127
- try {
128
- // @ts-ignore - optional dynamic import may fail at build if module not installed; fallback is fine
129
- // We keep fallback as default; runtime override attempted via dynamic import is not critical
130
- } catch {}
131
-
132
- // ---------------------------------------------------------------------------
133
- // Simple Input fallback — mimics @earendil-works/pi-tui Input for search mode
134
- // Handles printable chars, backspace (\x7f, \x08), delete, left/right arrows.
135
- // Propagates focused flag per Focusable contract. Fallback when pi-tui Input not resolvable.
136
-
137
- class SimpleSearchInput implements Component, Focusable {
138
- value = "";
139
- focused = false;
140
- private cursor = 0;
141
- // Keep for compatibility with pi-tui Input shape
142
- getValue(): string { return this.value; }
143
- setValue(v: string): void { this.value = v; this.cursor = v.length; }
144
-
145
- handleInput(data: string): void {
146
- if (!data) return;
147
- // Handle bracketed paste markers — strip and insert content
148
- if (data.includes("\x1b[200~")) {
149
- const start = data.indexOf("\x1b[200~");
150
- const end = data.indexOf("\x1b[201~");
151
- if (end !== -1) {
152
- const paste = data.substring(start + 6, end);
153
- this.insertAtCursor(paste);
154
- const remaining = data.slice(end + 6);
155
- if (remaining) this.handleInput(remaining);
156
- return;
157
- } else {
158
- // incomplete paste marker — strip marker and insert rest
159
- data = data.replace("\x1b[200~", "");
160
- }
161
- }
162
- if (data.includes("\x1b[201~")) {
163
- data = data.replace("\x1b[201~", "");
164
- }
165
-
166
- // Special keys — handle one at a time if data contains escapes
167
- // Backspace: \x7f or \x08 or \b
168
- if (data === "\x7f" || data === "\x08" || data === "\b") {
169
- this.handleBackspace();
170
- return;
171
- }
172
- // Delete (forward): \x1b[3~
173
- if (data === "\x1b[3~") {
174
- this.handleDelete();
175
- return;
176
- }
177
- // Arrows for cursor movement within input
178
- if (data === "\x1b[D" || data === "\x1bOD") { // left
179
- if (this.cursor > 0) this.cursor--;
180
- return;
181
- }
182
- if (data === "\x1b[C" || data === "\x1bOC") { // right
183
- if (this.cursor < this.value.length) this.cursor++;
184
- return;
185
- }
186
- if (data === "\x1b[H" || data === "\x1bOH" || data === "\x1b[1~") { // home
187
- this.cursor = 0;
188
- return;
189
- }
190
- if (data === "\x1b[F" || data === "\x1bOF" || data === "\x1b[4~") { // end
191
- this.cursor = this.value.length;
192
- return;
193
- }
194
- // Ctrl-U (kill line)
195
- if (data === "\x15") {
196
- this.value = "";
197
- this.cursor = 0;
198
- return;
199
- }
200
- // Ctrl-W (delete word backwards)
201
- if (data === "\x17") {
202
- this.deleteWordBackwards();
203
- return;
204
- }
205
- // If data looks like an escape sequence (arrow, etc.) not handled above and length >1, ignore
206
- if (data.startsWith("\x1b") && data.length > 1) {
207
- // Unknown escape — ignore (avoid inserting ESC into query)
208
- return;
209
- }
210
- // Ctrl-C / Ctrl-D etc — ignore
211
- if (data.length === 1 && data.charCodeAt(0) < 0x20) {
212
- return;
213
- }
214
- // Printable insertion — insert at cursor (support multi-char paste without bracket markers)
215
- // Filter out non-printable control chars
216
- let insert = "";
217
- for (const ch of data) {
218
- const code = ch.charCodeAt(0);
219
- if (code >= 0x20 && code !== 0x7f) insert += ch;
220
- }
221
- if (insert) this.insertAtCursor(insert);
222
- }
223
-
224
- private insertAtCursor(text: string): void {
225
- this.value = this.value.slice(0, this.cursor) + text + this.value.slice(this.cursor);
226
- this.cursor += text.length;
227
- }
228
- private handleBackspace(): void {
229
- if (this.cursor > 0) {
230
- this.value = this.value.slice(0, this.cursor - 1) + this.value.slice(this.cursor);
231
- this.cursor--;
232
- }
233
- }
234
- private handleDelete(): void {
235
- if (this.cursor < this.value.length) {
236
- this.value = this.value.slice(0, this.cursor) + this.value.slice(this.cursor + 1);
237
- }
238
- }
239
- private deleteWordBackwards(): void {
240
- if (this.cursor === 0) return;
241
- let pos = this.cursor;
242
- while (pos > 0 && this.value[pos - 1] === " ") pos--;
243
- while (pos > 0 && this.value[pos - 1] !== " ") pos--;
244
- this.value = this.value.slice(0, pos) + this.value.slice(this.cursor);
245
- this.cursor = pos;
246
- }
247
-
248
- render(width: number): string[] {
249
- // Render with cursor marker when focused. Use \x1b_pi:c\x07 marker for IME if needed? Simple underscore.
250
- // Show placeholder when empty and not focused? When focused, show cursor.
251
- const cursorMark = "\x1b_pi:c\x07";
252
- let display: string;
253
- if (this.focused) {
254
- const before = this.value.slice(0, this.cursor);
255
- const after = this.value.slice(this.cursor);
256
- // Insert cursor marker + visual block; terminal will show cursor at marker
257
- display = before + cursorMark + (after.length > 0 ? after : " ");
258
- // Also truncate to width with visibleWidth handling? Keep simple truncation
259
- // Use truncateFallback to keep ANSI safe (cursorMark is ANSI-like)
260
- if (visibleWidthFallback(display) > width) {
261
- // simple scroll: show window around cursor
262
- const half = Math.floor(width / 2);
263
- let start = Math.max(0, this.cursor - half);
264
- // crude slice
265
- let slice = this.value.slice(start, start + width);
266
- const curInSlice = this.cursor - start;
267
- display = slice.slice(0, curInSlice) + cursorMark + slice.slice(curInSlice);
268
- }
269
- } else {
270
- display = this.value;
271
- }
272
- // Apply truncate for width
273
- const line = truncateFallback(display, width);
274
- return [line];
275
- }
276
-
277
- invalidate(): void {}
278
- }
279
-
280
- // ---------------------------------------------------------------------------
281
-
282
- export interface ModelRow {
283
- id: string; // "provider/modelId"
284
- provider: string;
285
- modelId: string;
286
- visible: boolean;
287
- name?: string;
288
- }
289
-
290
- export interface ModelConfViewOpts {
291
- tui: any;
292
- theme: any;
293
- keybindings: any;
294
- allModels: Array<{ provider: string; id: string; name?: string }>;
295
- visibleSet: Set<string>;
296
- enabledRaw: string[] | undefined;
297
- onDone: (saved: boolean) => void;
298
- onPersist: (nextEnabled: string[] | undefined) => Promise<void>;
299
- allIds?: string[];
300
- }
301
-
302
- type ContentEntry =
303
- | { kind: "header"; provider: string }
304
- | { kind: "row"; row: ModelRow; flatIndex: number };
305
-
306
- export class ModelConfView implements Component, Focusable {
307
- private tui: any;
308
- private theme: any;
309
- private keybindings: any;
310
- private onDone: (saved: boolean) => void;
311
- private _onPersist: (next: string[] | undefined) => Promise<void>;
312
-
313
- private rows: ModelRow[] = [];
314
- private sortedProviders: string[] = [];
315
- private rowsByProvider: Map<string, ModelRow[]> = new Map();
316
- private cursorIndex: number = 0;
317
- private collapsed: Set<string> = new Set();
318
- private originalEnabled: string[] | undefined;
319
- private draftEnabled: string[] | undefined;
320
- private allIds: string[] = [];
321
- private pendingDiscard = false;
322
-
323
- private activeProviderIndex: number = 0;
324
- private perProviderCursor: Map<string, number> = new Map();
325
-
326
- private searchActive = false;
327
- private searchQuery = "";
328
- private filteredRows: ModelRow[] | null = null;
329
- private searchInput: SimpleSearchInput;
330
- private _focused = false;
331
- get focused(): boolean { return this._focused; }
332
- set focused(v: boolean) {
333
- this._focused = v;
334
- if (this.searchInput) this.searchInput.focused = v && this.searchActive;
335
- }
336
-
337
- private lastBulkMessage: string | null = null;
338
- private lastBulkTimer: ReturnType<typeof setTimeout> | null = null;
339
-
340
- constructor(opts: ModelConfViewOpts) {
341
- this.tui = opts.tui;
342
- this.theme = opts.theme;
343
- this.keybindings = opts.keybindings;
344
- this.onDone = opts.onDone;
345
- this._onPersist = opts.onPersist;
346
-
347
- this.searchInput = new SimpleSearchInput();
348
- try {
349
- const req: any = (globalThis as any).require ?? (typeof require !== "undefined" ? require : undefined);
350
- if (req) {
351
- let mod: any;
352
- try { mod = req("@earendil-works/pi-tui"); } catch { try { mod = req("@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui"); } catch {} }
353
- if (mod?.Input) {
354
- const real = new mod.Input();
355
- if (typeof real.value === "string" || typeof real.getValue === "function") {
356
- const wrapped: any = real;
357
- if (!("value" in wrapped) || typeof wrapped.value !== "string") {
358
- Object.defineProperty(wrapped, "value", {
359
- get() { return typeof wrapped.getValue === "function" ? wrapped.getValue() : wrapped._value ?? ""; },
360
- set(v: string) { if (typeof wrapped.setValue === "function") wrapped.setValue(v); else wrapped._value = v; },
361
- configurable: true,
362
- });
363
- }
364
- this.searchInput = wrapped;
365
- }
366
- }
367
- }
368
- } catch {}
369
-
370
- const all = opts.allModels ?? [];
371
- this.rows = all.map((m) => {
372
- const id = `${m.provider}/${m.id}`;
373
- return {
374
- id,
375
- provider: m.provider,
376
- modelId: m.id,
377
- visible: opts.visibleSet.has(id),
378
- name: m.name ?? m.id,
379
- } as ModelRow;
380
- });
381
-
382
- this.rows.sort((a, b) => {
383
- if (a.provider !== b.provider) return a.provider.localeCompare(b.provider);
384
- return a.modelId.localeCompare(b.modelId);
385
- });
386
-
387
- this.rowsByProvider = new Map();
388
- for (const r of this.rows) {
389
- const arr = this.rowsByProvider.get(r.provider);
390
- if (arr) arr.push(r);
391
- else this.rowsByProvider.set(r.provider, [r]);
392
- }
393
- this.sortedProviders = [...this.rowsByProvider.keys()].sort((a, b) => a.localeCompare(b));
394
-
395
- const ordered: ModelRow[] = [];
396
- for (const p of this.sortedProviders) {
397
- const grp = this.rowsByProvider.get(p)!;
398
- grp.sort((a, b) => a.modelId.localeCompare(b.modelId));
399
- ordered.push(...grp);
400
- }
401
- this.rows = ordered;
402
-
403
- this.activeProviderIndex = 0;
404
- this.perProviderCursor = new Map();
405
- for (const p of this.sortedProviders) this.perProviderCursor.set(p, 0);
406
- const initActive = this.activeProvider;
407
- if (initActive) {
408
- const base = this.rowsByProvider.get(initActive) ?? [];
409
- this.cursorIndex = base.length > 0 ? 0 : -1;
410
- } else {
411
- this.cursorIndex = this.rows.length > 0 ? 0 : -1;
412
- }
413
-
414
- this.allIds = opts.allIds ?? all.map((m) => `${m.provider}/${m.id}`);
415
- this.originalEnabled = opts.enabledRaw === undefined ? undefined : [...opts.enabledRaw];
416
- this.draftEnabled = opts.enabledRaw === undefined ? undefined : [...opts.enabledRaw];
417
- this.pendingDiscard = false;
418
- }
419
-
420
- getVisibleCount(): number {
421
- return this.rows.filter((r) => r.visible).length;
422
- }
423
-
424
- private isDirty(): boolean {
425
- const a = this.originalEnabled;
426
- const b = this.draftEnabled;
427
- if (a === undefined && b === undefined) return false;
428
- if (a === undefined || b === undefined) return true;
429
- if (a.length !== b.length) return true;
430
- const sa = [...a].sort();
431
- const sb = [...b].sort();
432
- return JSON.stringify(sa) !== JSON.stringify(sb);
433
- }
434
-
435
- private rebuildVisibility(): void {
436
- const draft = this.draftEnabled;
437
- if (draft === undefined || draft.length === 0) {
438
- for (const r of this.rows) r.visible = true;
439
- return;
440
- }
441
- const hidden = draft as string[];
442
- const VALID_THINKING = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
443
- const stripThinking = (p: string) => {
444
- const idx = p.lastIndexOf(":");
445
- if (idx !== -1 && VALID_THINKING.has(p.slice(idx + 1))) return p.slice(0, idx);
446
- return p;
447
- };
448
- const matchesPattern = (ref: string, pattern: string): boolean => {
449
- const pat = stripThinking(pattern);
450
- const lowerRef = ref.toLowerCase();
451
- const lowerPat = pat.toLowerCase();
452
- const bareId = ref.split("/").slice(1).join("/").toLowerCase();
453
- const hasGlob = /[*?[\]]/.test(pat);
454
- if (hasGlob) {
455
- const rx = globToRegExp(pat);
456
- if (rx.test(ref)) return true;
457
- if (rx.test(bareId)) return true;
458
- return false;
459
- }
460
- return lowerRef === lowerPat || bareId === lowerPat;
461
- };
462
- for (const r of this.rows) {
463
- let isHidden = false;
464
- for (const pat of hidden) {
465
- if (matchesPattern(r.id, pat)) { isHidden = true; break; }
466
- }
467
- r.visible = !isHidden;
468
- }
469
- }
470
-
471
- private safeFg(color: string, text: string): string {
472
- try {
473
- if (this.theme && typeof this.theme.fg === "function") {
474
- return this.theme.fg(color, text);
475
- }
476
- } catch {}
477
- return text;
478
- }
479
-
480
- private safeBold(text: string): string {
481
- try {
482
- if (this.theme && typeof this.theme.bold === "function") {
483
- return this.theme.bold(text);
484
- }
485
- } catch {}
486
- return text;
487
- }
488
-
489
- private safeBg(color: string, text: string): string {
490
- try {
491
- if (this.theme && typeof this.theme.bg === "function") {
492
- return this.theme.bg(color, text);
493
- }
494
- } catch {}
495
- return text;
496
- }
497
-
498
- private highlight(text: string): string {
499
- try {
500
- if (this.theme && typeof this.theme.bg === "function") {
501
- try {
502
- const v = this.theme.bg("selectedBg", text);
503
- if (v !== text) return v;
504
- } catch {}
505
- try {
506
- const v2 = (this.theme as any).bg("selection", text);
507
- if (v2 !== text) return v2;
508
- } catch {}
509
- }
510
- } catch {}
511
- try {
512
- if (this.theme && typeof this.theme.fg === "function") {
513
- return this.theme.fg("accent", text);
514
- }
515
- } catch {}
516
- return `› ${text}`;
517
- }
518
-
519
- private isProviderCollapsed(provider: string): boolean {
520
- return this.collapsed.has(provider);
521
- }
522
-
523
- private get activeProvider(): string | undefined {
524
- if (this.sortedProviders.length === 0) return undefined;
525
- const idx = Math.max(0, Math.min(this.activeProviderIndex, this.sortedProviders.length - 1));
526
- return this.sortedProviders[idx];
527
- }
528
-
529
- private get activeRows(): ModelRow[] {
530
- const p = this.activeProvider;
531
- if (!p) return [];
532
- return this.rowsByProvider.get(p) ?? [];
533
- }
534
-
535
- private get rowsToShow(): ModelRow[] {
536
- const base = this.activeRows;
537
- if (this.filteredRows !== null) {
538
- const filteredForActive = this.filteredRows.filter((r) => r.provider === this.activeProvider);
539
- if (filteredForActive.length !== this.filteredRows.length) return filteredForActive;
540
- return this.filteredRows;
541
- }
542
- return base;
543
- }
544
-
545
- private cursorRow(): ModelRow | undefined {
546
- const list = this.rowsToShow;
547
- if (this.cursorIndex < 0 || this.cursorIndex >= list.length) return undefined;
548
- return list[this.cursorIndex];
549
- }
550
-
551
- private getCurrentProvider(): string | undefined {
552
- return this.activeProvider;
553
- }
554
-
555
- private switchProvider(delta: number): void {
556
- if (this.sortedProviders.length <= 1) return;
557
- const curProvider = this.activeProvider;
558
- if (curProvider) this.perProviderCursor.set(curProvider, this.cursorIndex);
559
- let next = this.activeProviderIndex + delta;
560
- next = (next + this.sortedProviders.length) % this.sortedProviders.length;
561
- this.activeProviderIndex = next;
562
- const nextProvider = this.activeProvider!;
563
- let saved = this.perProviderCursor.get(nextProvider);
564
- if (saved === undefined) saved = 0;
565
- const targetRows = this.rowsToShowForProvider(nextProvider);
566
- if (targetRows.length === 0) this.cursorIndex = -1;
567
- else this.cursorIndex = Math.max(0, Math.min(saved, targetRows.length - 1));
568
- if (this.filteredRows !== null && this.searchQuery.trim()) {
569
- this.recomputeFilteredForActive();
570
- const after = this.rowsToShow;
571
- if (after.length === 0) this.cursorIndex = -1;
572
- else this.cursorIndex = Math.max(0, Math.min(this.cursorIndex, after.length - 1));
573
- }
574
- }
575
-
576
- private applyVisibleBulk(action: "include" | "exclude"): void {
577
- const list = this.rowsToShow;
578
- if (list.length === 0) return;
579
- const ids = list.map((r) => `${r.provider}/${r.modelId}` as ModelId);
580
- this.draftEnabled = applyBulk(this.draftEnabled as any, this.allIds as unknown as ModelId[], ids, action) as any;
581
- this.rebuildVisibility();
582
- const verb = action === "include" ? "Included" : "Excluded";
583
- const prov = this.activeProvider ?? "";
584
- this.lastBulkMessage = `${verb} ${ids.length} in ${prov}`;
585
- if (this.lastBulkTimer) { try { clearTimeout(this.lastBulkTimer); } catch {} }
586
- this.lastBulkTimer = setTimeout(() => { this.lastBulkMessage = null; try { this.tui?.requestRender?.(); } catch {} }, 4000) as any;
587
- if (this.cursorIndex >= this.rowsToShow.length) this.cursorIndex = Math.max(0, this.rowsToShow.length - 1);
588
- if (this.activeProvider) this.perProviderCursor.set(this.activeProvider, this.cursorIndex);
589
- try { this.tui?.requestRender?.(); } catch {}
590
- }
591
-
592
- private rowsToShowForProvider(provider: string): ModelRow[] {
593
- const base = this.rowsByProvider.get(provider) ?? [];
594
- if (this.filteredRows !== null) {
595
- const anyForProvider = this.filteredRows.some((r) => r.provider === provider);
596
- if (anyForProvider) return this.filteredRows.filter((r) => r.provider === provider);
597
- if (this.searchQuery.trim()) {
598
- try {
599
- return fuzzyFilter(this.searchQuery, base, (r) => `${r.provider}/${r.modelId} ${r.name ?? ""}`);
600
- } catch { return []; }
601
- }
602
- return [];
603
- }
604
- return base;
605
- }
606
-
607
- private recomputeFilteredForActive(): void {
608
- const q = this.searchQuery;
609
- if (!q.trim()) {
610
- this.filteredRows = null;
611
- return;
612
- }
613
- const base = this.activeRows;
614
- try {
615
- this.filteredRows = fuzzyFilter(q, base, (r) => `${r.provider}/${r.modelId} ${r.name ?? ""}`);
616
- } catch {
617
- this.filteredRows = null;
618
- }
619
- }
620
-
621
- private renderTabBar(width: number): string {
622
- if (this.sortedProviders.length === 0) return this.safeFg("dim", " (no providers)");
623
- const tabs: { styled: string; width: number }[] = [];
624
- for (let i = 0; i < this.sortedProviders.length; i++) {
625
- const p = this.sortedProviders[i];
626
- const group = this.rowsByProvider.get(p) ?? [];
627
- const vis = group.filter((r) => r.visible).length;
628
- const total = group.length;
629
- const count = `${vis}/${total}`;
630
- const rawLabel = `${p} ${count}`;
631
- let styled: string;
632
- if (i === this.activeProviderIndex) {
633
- const inner = ` [${rawLabel}] `;
634
- let hl = inner;
635
- try {
636
- const bgged = this.safeBg("selection", inner);
637
- if (bgged !== inner) hl = bgged;
638
- else hl = this.safeFg("accent", this.safeBold(inner));
639
- } catch {
640
- hl = this.safeFg("accent", this.safeBold(inner));
641
- }
642
- styled = hl;
643
- } else {
644
- styled = this.safeFg("dim", ` [${rawLabel}] `);
645
- }
646
- const w = visibleWidthFallback(stripAnsi(styled));
647
- tabs.push({ styled, width: w });
648
- }
649
- let totalW = tabs.reduce((s, t) => s + t.width, 0) + Math.max(0, tabs.length - 1);
650
- if (totalW <= width) {
651
- return tabs.map((t) => t.styled).join("");
652
- }
653
- let start = this.activeProviderIndex;
654
- let end = this.activeProviderIndex + 1;
655
- let curW = tabs[this.activeProviderIndex].width;
656
- let left = this.activeProviderIndex - 1;
657
- let right = this.activeProviderIndex + 1;
658
- while (left >= 0 || right < tabs.length) {
659
- let expanded = false;
660
- if (left >= 0) {
661
- const need = curW + 1 + tabs[left].width;
662
- if (need <= width - 4) {
663
- curW = need;
664
- start = left;
665
- left--;
666
- expanded = true;
667
- }
668
- }
669
- if (right < tabs.length) {
670
- const need = curW + 1 + tabs[right].width;
671
- if (need <= width - 4) {
672
- curW = need;
673
- end = right + 1;
674
- right++;
675
- expanded = true;
676
- }
677
- }
678
- if (!expanded) break;
679
- }
680
- const visible = tabs.slice(start, end);
681
- let out = visible.map((t) => t.styled).join("");
682
- if (start > 0) out = this.safeFg("dim", "‹ ") + out;
683
- if (end < tabs.length) out = out + this.safeFg("dim", " ›");
684
- return this.truncate(out, width);
685
- }
686
-
687
- private getAvailableHeight(): number {
688
- let h: number | undefined;
689
- try {
690
- const term = (this.tui as any)?.terminal;
691
- if (term) {
692
- if (typeof term.rows === "number") h = term.rows;
693
- else if (typeof term.height === "number") h = term.height;
694
- else if (typeof term.getSize === "function") {
695
- const sz = term.getSize();
696
- if (sz && typeof sz.rows === "number") h = sz.rows;
697
- }
698
- }
699
- } catch {}
700
- if (h === undefined || h === null) {
701
- try {
702
- const c: any = (globalThis as any).process?.stdout;
703
- if (c && typeof c.rows === "number") h = c.rows;
704
- } catch {}
705
- }
706
- if (h === undefined || h === null || h <= 0) h = 24;
707
- const searchExtra = this.searchActive ? 1 : (this.filteredRows !== null && this.searchQuery ? 1 : 0);
708
- // Half-screen: reserve half the terminal height for the list
709
- const half = Math.floor(h / 2);
710
- // Account for chrome: title + 2 dividers + tab bar + blank spacer + status + 2-line footer
711
- const chrome = 8 + searchExtra;
712
- const avail = Math.max(5, half - chrome);
713
- return Math.min(avail, 20);
714
- }
715
-
716
- private truncate(text: string, width: number): string {
717
- try {
718
- return truncateToWidthFn(text, width);
719
- } catch {
720
- return truncateFallback(text, width);
721
- }
722
- }
723
-
724
- render(width: number): string[] {
725
- const lines: string[] = [];
726
- const total = this.rows.length;
727
- const visible = this.getVisibleCount();
728
- const dirtyMark = this.isDirty() ? this.safeFg("warning", " ● unsaved") : "";
729
- const titleText = this.safeFg("accent", this.safeBold("modelconf")) + this.safeFg("dim", ` ${total} models ${visible} visible`) + dirtyMark;
730
- lines.push(this.truncate(titleText, width));
731
- lines.push(this.truncate(this.safeFg("dim", "─".repeat(Math.max(0, width))), width));
732
- lines.push(this.truncate(this.renderTabBar(width), width));
733
- lines.push(this.truncate(this.safeFg("dim", "─".repeat(Math.max(0, width))), width));
734
- if (this.searchActive) {
735
- const inputWidth = Math.max(10, width - 4);
736
- const rendered = this.searchInput.render(inputWidth);
737
- const inputLine = rendered[0] ?? (this.searchInput as any).value ?? "";
738
- const prefix = this.safeFg("accent", "/ ");
739
- const line = prefix + inputLine;
740
- lines.push(this.truncate(line, width));
741
- } else if (this.filteredRows !== null && this.searchQuery) {
742
- const hint = this.safeFg("dim", `filtered in ${this.activeProvider ?? ""}: "${this.searchQuery}" (${this.rowsToShow.length}/${this.activeRows.length}) • Esc to clear • / to search`);
743
- lines.push(this.truncate(hint, width));
744
- }
745
- const rowsToShow = this.rowsToShow;
746
- if (rowsToShow.length === 0) {
747
- if (this.filteredRows !== null) {
748
- const q = this.searchQuery;
749
- const prov = this.activeProvider ?? "provider";
750
- lines.push(this.truncate(this.safeFg("warning", ` No models match '${q}' in ${prov}`), width));
751
- lines.push(this.truncate(this.safeFg("dim", " Press Esc to clear filter"), width));
752
- } else if (this.sortedProviders.length === 0) {
753
- lines.push(this.truncate(this.safeFg("dim", " No providers configured"), width));
754
- } else {
755
- lines.push(this.truncate(this.safeFg("dim", ` No models in ${this.activeProvider ?? "provider"}`), width));
756
- }
757
- } else {
758
- const maxVisible = this.getAvailableHeight();
759
- let start = 0;
760
- let end = rowsToShow.length;
761
- if (rowsToShow.length > maxVisible) {
762
- const cursor = this.cursorIndex;
763
- const half = Math.floor(maxVisible / 2);
764
- start = Math.max(0, Math.min(cursor - half, rowsToShow.length - maxVisible));
765
- end = Math.min(start + maxVisible, rowsToShow.length);
766
- }
767
- for (let idx = start; idx < end; idx++) {
768
- const row = rowsToShow[idx];
769
- const isCursor = this.cursorIndex === idx;
770
- const mark = row.visible ? this.safeFg("success", "◉") : this.safeFg("dim", "○");
771
- const labelPart = `${mark} ${row.modelId}`;
772
- const dimName = row.name && row.name !== row.modelId ? this.safeFg("dim", ` ${row.name}`) : "";
773
- let rawLine = `${labelPart}${dimName}`;
774
- rawLine = ` ${rawLine}`;
775
- let rendered: string;
776
- if (isCursor) {
777
- const highlighted = this.highlight(rawLine);
778
- rendered = this.truncate(highlighted, width);
779
- } else {
780
- rendered = this.truncate(rawLine, width);
781
- }
782
- lines.push(rendered);
783
- }
784
- if (rowsToShow.length > maxVisible) {
785
- const pos = this.cursorIndex >= 0 ? this.cursorIndex + 1 : 0;
786
- const indicator = this.safeFg("dim", ` (${pos}/${rowsToShow.length}) in ${this.activeProvider ?? ""}`);
787
- lines.push(this.truncate(indicator, width));
788
- }
789
- }
790
- lines.push("");
791
- if (this.pendingDiscard) {
792
- const msg = this.safeFg("warning", "Discard changes? (y/N)") + this.safeFg("dim", " y: discard n: keep editing Esc: discard");
793
- lines.push(this.truncate(msg, width));
794
- } else if (this.lastBulkMessage) {
795
- const isInc = this.lastBulkMessage.startsWith("Included") || this.lastBulkMessage.startsWith("Enabled");
796
- const bulk = this.safeFg(isInc ? "success" : "warning", this.lastBulkMessage) + this.safeFg("dim", this.isDirty() ? " • ● unsaved — Enter save" : "");
797
- lines.push(this.truncate(bulk, width));
798
- } else if (this.isDirty()) {
799
- lines.push(this.truncate(this.safeFg("warning", "● unsaved — press Enter to save"), width));
800
- } else if (this.searchActive) {
801
- lines.push(this.truncate(this.safeFg("dim", "filtering — Esc clear • Enter keep filter"), width));
802
- } else {
803
- const countLine = this.safeFg("dim", `${visible} visible / ${total} total`);
804
- lines.push(this.truncate(countLine, width));
805
- }
806
- // --- Persistent keybinds footer — always visible at bottom (all binds) ---
807
- const kb1 = "Tab/⇧Tab ←→ switch provider │ ↑↓/j k Home/End navigate │ Space toggle │ / filter";
808
- const kb2 = "a add visible │ x hide visible │ d toggle provider │ Enter save │ Esc/q close";
809
- const sep = this.safeFg("dim", " │ ");
810
- // If terminal is wide enough, show in one line; otherwise split into two
811
- const oneLine = `${kb1} │ ${kb2}`;
812
- let footerLines: string[];
813
- try {
814
- const w = visibleWidthFallback(stripAnsi(oneLine));
815
- if (w + 2 <= width) {
816
- footerLines = [this.safeFg("dim", oneLine)];
817
- } else {
818
- footerLines = [this.safeFg("dim", kb1), this.safeFg("dim", kb2)];
819
- }
820
- } catch {
821
- footerLines = [this.safeFg("dim", kb1), this.safeFg("dim", kb2)];
822
- }
823
- for (const fl of footerLines) lines.push(this.truncate(fl, width));
824
- return lines.map((l) => this.truncate(l, width));
825
- }
826
-
827
- handleInput(data: string): void {
828
- if (!data) return;
829
- const tryKb = (keyId: string): boolean => {
830
- try {
831
- if (this.keybindings) {
832
- if (typeof this.keybindings.matches === "function" && this.keybindings.matches(data, keyId)) return true;
833
- if (typeof this.keybindings.match === "function" && this.keybindings.match(data, keyId)) return true;
834
- }
835
- } catch {}
836
- return false;
837
- };
838
- if (this.pendingDiscard) {
839
- if (data === "y" || data === "Y") {
840
- this.pendingDiscard = false;
841
- this.onDone(false);
842
- return;
843
- }
844
- if (data === "n" || data === "N") {
845
- this.pendingDiscard = false;
846
- try { this.tui?.requestRender?.(); } catch {}
847
- return;
848
- }
849
- const isEscPend = data === "\x1b" || tryKb("escape") || data.includes("\x1b[27");
850
- if (isEscPend) {
851
- this.pendingDiscard = false;
852
- this.onDone(false);
853
- return;
854
- }
855
- return;
856
- }
857
- if (!this.searchActive && data === "/") {
858
- this.searchActive = true;
859
- this.searchInput.focused = true;
860
- this._focused = true;
861
- if (typeof (this.searchInput as any).setValue === "function") (this.searchInput as any).setValue("");
862
- else (this.searchInput as any).value = "";
863
- this.searchQuery = "";
864
- if (this.activeProvider) this.perProviderCursor.set(this.activeProvider, this.cursorIndex);
865
- this.cursorIndex = this.rowsToShow.length > 0 ? 0 : -1;
866
- try { this.tui?.requestRender?.(); } catch {}
867
- return;
868
- }
869
- if (this.searchActive) {
870
- // Arrow/Home/End navigate the filtered list while typing (left/right still edit the query).
871
- const navUp = data === "\x1b[A" || data === "\x1bOA" || tryKb("up");
872
- const navDown = data === "\x1b[B" || data === "\x1bOB" || tryKb("down");
873
- const navHome = data === "\x1b[H" || data === "\x1b[1~" || data === "\x1bOH" || tryKb("home");
874
- const navEnd = data === "\x1b[F" || data === "\x1b[4~" || data === "\x1bOF" || tryKb("end");
875
- if (navUp || navDown || navHome || navEnd) {
876
- const navList = this.rowsToShow;
877
- if (navList.length > 0) {
878
- if (navUp) this.cursorIndex = (this.cursorIndex - 1 + navList.length) % navList.length;
879
- else if (navDown) this.cursorIndex = (this.cursorIndex + 1) % navList.length;
880
- else if (navHome) this.cursorIndex = 0;
881
- else this.cursorIndex = navList.length - 1;
882
- if (this.activeProvider) this.perProviderCursor.set(this.activeProvider, this.cursorIndex);
883
- }
884
- try { this.tui?.requestRender?.(); } catch {}
885
- return;
886
- }
887
- const isEscKitty = data.includes("\x1b[27");
888
- const isEsc = data === "\x1b" || tryKb("escape") || isEscKitty;
889
- const isEnter = data === "\r" || data === "\n" || data === "\r\n" || data.includes("\r") || data.includes("\n") || tryKb("enter") || tryKb("return");
890
- if (data === "\t" || data === "\x1b[Z") {
891
- try { this.tui?.requestRender?.(); } catch {}
892
- return;
893
- }
894
- if (isEsc) {
895
- this.searchActive = false;
896
- this.searchInput.focused = false;
897
- this.filteredRows = null;
898
- this.searchQuery = "";
899
- if (typeof (this.searchInput as any).setValue === "function") (this.searchInput as any).setValue("");
900
- else (this.searchInput as any).value = "";
901
- const prov = this.activeProvider;
902
- if (prov) {
903
- const saved = this.perProviderCursor.get(prov);
904
- const list = this.rowsToShow;
905
- if (list.length === 0) this.cursorIndex = -1;
906
- else this.cursorIndex = saved !== undefined ? Math.max(0, Math.min(saved, list.length - 1)) : 0;
907
- } else {
908
- this.cursorIndex = this.rowsToShow.length > 0 ? 0 : -1;
909
- }
910
- try { this.tui?.requestRender?.(); } catch {}
911
- return;
912
- } else if (isEnter) {
913
- this.searchActive = false;
914
- this.searchInput.focused = false;
915
- if (this.activeProvider) this.perProviderCursor.set(this.activeProvider, this.cursorIndex);
916
- try { this.tui?.requestRender?.(); } catch {}
917
- return;
918
- } else {
919
- try { (this.searchInput as any).handleInput?.(data); } catch {}
920
- let q: string = "";
921
- try {
922
- const si: any = this.searchInput;
923
- if (typeof si.getValue === "function") q = si.getValue();
924
- else if (typeof si.value === "string") q = si.value;
925
- else q = String(si.value ?? "");
926
- } catch { q = (this.searchInput as any).value ?? ""; }
927
- this.searchQuery = q;
928
- if (!q.trim()) {
929
- this.filteredRows = null;
930
- const prov = this.activeProvider;
931
- if (prov) {
932
- const saved = this.perProviderCursor.get(prov);
933
- this.cursorIndex = saved !== undefined ? Math.max(0, Math.min(saved, this.activeRows.length - 1)) : 0;
934
- if (this.activeRows.length === 0) this.cursorIndex = -1;
935
- } else {
936
- this.cursorIndex = 0;
937
- }
938
- } else {
939
- try {
940
- const base = this.activeRows;
941
- this.filteredRows = fuzzyFilter(q, base, (r) => `${r.provider}/${r.modelId} ${r.name ?? ""}`);
942
- } catch {
943
- this.filteredRows = null;
944
- }
945
- this.cursorIndex = this.rowsToShow.length > 0 ? 0 : -1;
946
- }
947
- try { this.tui?.requestRender?.(); } catch {}
948
- return;
949
- }
950
- }
951
- if (data === "\t" || data === "\x09") {
952
- this.switchProvider(1);
953
- try { this.tui?.requestRender?.(); } catch {}
954
- return;
955
- }
956
- if (data === "\x1b[Z") {
957
- this.switchProvider(-1);
958
- try { this.tui?.requestRender?.(); } catch {}
959
- return;
960
- }
961
- if (!this.searchActive && (data === "d" || data === "D")) {
962
- const active = this.activeRows;
963
- if (active.length === 0) return;
964
- const vis = active.filter((r) => r.visible).length;
965
- const allVisible = vis === active.length;
966
- const ids = active.map((r) => `${r.provider}/${r.modelId}` as ModelId);
967
- if (allVisible) {
968
- this.draftEnabled = applyBulk(this.draftEnabled as any, this.allIds as unknown as ModelId[], ids, "exclude") as any;
969
- this.lastBulkMessage = `Disabled all ${ids.length} in ${this.activeProvider ?? "provider"}`;
970
- } else {
971
- this.draftEnabled = applyBulk(this.draftEnabled as any, this.allIds as unknown as ModelId[], ids, "include") as any;
972
- this.lastBulkMessage = `Enabled all ${ids.length} in ${this.activeProvider ?? "provider"}`;
973
- }
974
- this.rebuildVisibility();
975
- if (this.lastBulkTimer) { try { clearTimeout(this.lastBulkTimer); } catch {} }
976
- this.lastBulkTimer = setTimeout(() => { this.lastBulkMessage = null; try { this.tui?.requestRender?.(); } catch {} }, 4000) as any;
977
- if (this.cursorIndex >= this.rowsToShow.length) this.cursorIndex = Math.max(0, this.rowsToShow.length - 1);
978
- if (this.activeProvider) this.perProviderCursor.set(this.activeProvider, this.cursorIndex);
979
- try { this.tui?.requestRender?.(); } catch {}
980
- return;
981
- }
982
- if (!this.searchActive && (data === "a" || data === "A")) {
983
- this.applyVisibleBulk("include");
984
- return;
985
- }
986
- if (!this.searchActive && (data === "x" || data === "X")) {
987
- this.applyVisibleBulk("exclude");
988
- return;
989
- }
990
- const isEscKitty = data.includes("\x1b[27");
991
- const isEsc = data === "\x1b" || tryKb("escape") || isEscKitty;
992
- const isQ = data === "q" || data === "Q";
993
- if ((isEsc || isQ) && this.filteredRows !== null) {
994
- this.filteredRows = null;
995
- this.searchQuery = "";
996
- if (typeof (this.searchInput as any).setValue === "function") (this.searchInput as any).setValue("");
997
- else (this.searchInput as any).value = "";
998
- this.searchInput.focused = false;
999
- this.searchActive = false;
1000
- const prov = this.activeProvider;
1001
- if (prov) {
1002
- const saved = this.perProviderCursor.get(prov);
1003
- const list = this.activeRows;
1004
- if (list.length === 0) this.cursorIndex = -1;
1005
- else this.cursorIndex = saved !== undefined ? Math.max(0, Math.min(saved, list.length - 1)) : 0;
1006
- } else {
1007
- this.cursorIndex = this.rowsToShow.length > 0 ? 0 : -1;
1008
- }
1009
- try { this.tui?.requestRender?.(); } catch {}
1010
- return;
1011
- }
1012
- if (isEsc || isQ) {
1013
- if (this.isDirty()) {
1014
- this.pendingDiscard = true;
1015
- try { this.tui?.requestRender?.(); } catch {}
1016
- return;
1017
- }
1018
- this.onDone(false);
1019
- return;
1020
- }
1021
- if (data === " ") {
1022
- const row = this.cursorRow();
1023
- if (!row) return;
1024
- const modelId = `${row.provider}/${row.modelId}` as ModelId;
1025
- // Hide/show operates on the full model list (this.allIds is the full registry),
1026
- // even though the view is scoped to configured providers. This ensures toggling
1027
- // a model affects the main full enabledModels list, not just the scoped subset.
1028
- this.draftEnabled = toggleInEnabled(modelId, this.draftEnabled, this.allIds as ModelId[]);
1029
- this.rebuildVisibility();
1030
- this.pendingDiscard = false;
1031
- if (this.activeProvider) this.perProviderCursor.set(this.activeProvider, this.cursorIndex);
1032
- try { this.tui?.requestRender?.(); } catch {}
1033
- return;
1034
- }
1035
- const isEnter = data === "\r" || data === "\n" || data === "\r\n" || tryKb("enter") || tryKb("return");
1036
- const isEnterRaw = isEnter || data.includes("\r") || data.includes("\n");
1037
- if (isEnterRaw) {
1038
- const toSave = this.draftEnabled;
1039
- Promise.resolve(this._onPersist(toSave as any)).then(() => {
1040
- this.onDone(true);
1041
- }).catch(() => {
1042
- try { this.tui?.requestRender?.(); } catch {}
1043
- });
1044
- return;
1045
- }
1046
- // Left/Right also switch provider (alternative to Tab/Shift-Tab) when not in search
1047
- const isLeft = data === "\x1b[D" || data === "\x1bOD" || tryKb("left");
1048
- const isRight = data === "\x1b[C" || data === "\x1bOC" || tryKb("right");
1049
- if (isLeft) {
1050
- this.switchProvider(-1);
1051
- try { this.tui?.requestRender?.(); } catch {}
1052
- return;
1053
- }
1054
- if (isRight) {
1055
- this.switchProvider(1);
1056
- try { this.tui?.requestRender?.(); } catch {}
1057
- return;
1058
- }
1059
- let handled = false;
1060
- const isUp = data === "k" || data === "\x1b[A" || data === "\x1bOA" || tryKb("up") || tryKb("k");
1061
- const isDown = data === "j" || data === "\x1b[B" || data === "\x1bOB" || tryKb("down") || tryKb("j");
1062
- const isHome = data === "g" || data === "\x1b[H" || data === "\x1b[1~" || data === "\x1bOH" || tryKb("home") || tryKb("g");
1063
- const isEnd = data === "G" || data === "\x1b[F" || data === "\x1b[4~" || data === "\x1bOF" || tryKb("end") || tryKb("G");
1064
- const navList = this.rowsToShow;
1065
- if (isUp) {
1066
- if (navList.length > 0) {
1067
- const next = (this.cursorIndex - 1 + navList.length) % navList.length;
1068
- this.cursorIndex = next;
1069
- if (this.activeProvider) this.perProviderCursor.set(this.activeProvider, this.cursorIndex);
1070
- handled = true;
1071
- }
1072
- } else if (isDown) {
1073
- if (navList.length > 0) {
1074
- const next = (this.cursorIndex + 1) % navList.length;
1075
- this.cursorIndex = next;
1076
- if (this.activeProvider) this.perProviderCursor.set(this.activeProvider, this.cursorIndex);
1077
- handled = true;
1078
- }
1079
- } else if (isHome) {
1080
- if (navList.length > 0) {
1081
- this.cursorIndex = 0;
1082
- if (this.activeProvider) this.perProviderCursor.set(this.activeProvider, this.cursorIndex);
1083
- handled = true;
1084
- }
1085
- } else if (isEnd) {
1086
- if (navList.length > 0) {
1087
- this.cursorIndex = navList.length - 1;
1088
- if (this.activeProvider) this.perProviderCursor.set(this.activeProvider, this.cursorIndex);
1089
- handled = true;
1090
- }
1091
- }
1092
- if (handled) {
1093
- try { this.tui?.requestRender?.(); } catch {}
1094
- }
1095
- }
1096
-
1097
- invalidate(): void {
1098
- try { this.tui?.requestRender?.(); } catch {}
1099
- }
1100
- }
1101
-
1
+ import { applyBulk, toggleInEnabled } from "../persistence.js";
2
+ import type { ModelId } from "../persistence.js";
3
+ import { globToRegExp } from "../glob.js";
4
+ import { fuzzyFilter } from "../fuzzy.js";
5
+ // Minimal Component type to avoid requiring @earendil-works/pi-tui at build time.
6
+ // We mirror the pi-tui Component contract used by ctx.ui.custom.
7
+ export interface Component {
8
+ render(width: number): string[];
9
+ handleInput?(data: string): void;
10
+ invalidate(): void;
11
+ }
12
+
13
+ // Focusable contract per docs/tui.md — propagate focused to child Input
14
+ export interface Focusable {
15
+ focused: boolean;
16
+ }
17
+
18
+ // ---- local helpers for width/truncation (fallback when pi-tui not available) ----
19
+
20
+ function stripAnsi(s: string): string {
21
+ // strip CSI ...m, OSC 8 hyperlink, and APC cursor marker
22
+ return s.replace(/\x1b\[[0-9;]*m/g, "").replace(/\x1b\]8;;.*?\x1b\\/g, "").replace(/\x1b_pi:c\x07/g, "");
23
+ }
24
+
25
+ function visibleWidthFallback(s: string): number {
26
+ const stripped = stripAnsi(s);
27
+ let w = 0;
28
+ for (const ch of stripped) {
29
+ const cp = ch.codePointAt(0) ?? 0;
30
+ // crude wide char detection: CJK + emoji ranges
31
+ if (cp >= 0x1100 && (
32
+ cp <= 0x115F || cp === 0x2329 || cp === 0x232A ||
33
+ (cp >= 0x2E80 && cp <= 0xA4CF && cp !== 0x303F) ||
34
+ (cp >= 0xAC00 && cp <= 0xD7A3) ||
35
+ (cp >= 0xF900 && cp <= 0xFAFF) ||
36
+ (cp >= 0xFE10 && cp <= 0xFE19) ||
37
+ (cp >= 0xFE30 && cp <= 0xFE6F) ||
38
+ (cp >= 0xFF00 && cp <= 0xFF60) ||
39
+ (cp >= 0xFFE0 && cp <= 0xFFE6)
40
+ )) {
41
+ w += 2;
42
+ } else {
43
+ w += 1;
44
+ }
45
+ }
46
+ return w;
47
+ }
48
+
49
+ function extractAnsiCode(str: string, pos: number): { code: string; length: number } | null {
50
+ if (str[pos] !== "\x1b") return null;
51
+ const next = str[pos + 1];
52
+ if (next === "[") {
53
+ let i = pos + 2;
54
+ while (i < str.length) {
55
+ const c = str[i];
56
+ if ((c >= "0" && c <= "9") || c === ";" || c === ":" || c === "?" ) { i++; continue; }
57
+ // final char
58
+ i++;
59
+ return { code: str.slice(pos, i), length: i - pos };
60
+ }
61
+ return null;
62
+ }
63
+ if (next === "]") {
64
+ const end = str.indexOf("\x1b\\", pos);
65
+ if (end !== -1) return { code: str.slice(pos, end + 2), length: end + 2 - pos };
66
+ const bel = str.indexOf("\x07", pos);
67
+ if (bel !== -1) return { code: str.slice(pos, bel + 1), length: bel + 1 - pos };
68
+ return null;
69
+ }
70
+ if (str.startsWith("\x1b_pi:c\x07", pos)) return { code: "\x1b_pi:c\x07", length: 7 };
71
+ return null;
72
+ }
73
+
74
+ function truncateFallback(text: string, maxWidth: number, ellipsis = "..."): string {
75
+ if (maxWidth <= 0) return "";
76
+ const vw = visibleWidthFallback(text);
77
+ if (vw <= maxWidth) return text;
78
+ const ellW = visibleWidthFallback(ellipsis);
79
+ const target = maxWidth - ellW;
80
+ if (target <= 0) {
81
+ // truncate ellipsis itself if needed
82
+ let out = "";
83
+ let w = 0;
84
+ let i = 0;
85
+ while (i < text.length) {
86
+ const ansi = extractAnsiCode(text, i);
87
+ if (ansi) { out += ansi.code; i += ansi.length; continue; }
88
+ // grapheme: take one codepoint
89
+ const ch = text[i];
90
+ // handle surrogate pair
91
+ let grapheme = ch;
92
+ if (ch.charCodeAt(0) >= 0xD800 && ch.charCodeAt(0) <= 0xDBFF && i + 1 < text.length) {
93
+ grapheme = text.slice(i, i + 2);
94
+ }
95
+ const gw = visibleWidthFallback(grapheme);
96
+ if (w + gw > maxWidth) break;
97
+ out += grapheme;
98
+ w += gw;
99
+ i += grapheme.length;
100
+ }
101
+ return out;
102
+ }
103
+ let out = "";
104
+ let w = 0;
105
+ let i = 0;
106
+ let pendingAnsi = "";
107
+ while (i < text.length) {
108
+ const ansi = extractAnsiCode(text, i);
109
+ if (ansi) { pendingAnsi += ansi.code; out += ansi.code; i += ansi.length; continue; }
110
+ let grapheme = text[i];
111
+ if (grapheme.charCodeAt(0) >= 0xD800 && grapheme.charCodeAt(0) <= 0xDBFF && i + 1 < text.length) {
112
+ grapheme = text.slice(i, i + 2);
113
+ }
114
+ const gw = visibleWidthFallback(grapheme);
115
+ if (w + gw > target) break;
116
+ out += grapheme;
117
+ w += gw;
118
+ i += grapheme.length;
119
+ }
120
+ out += ellipsis;
121
+ // Append pending ansi resets? naive: ensure reset at end not needed
122
+ return out;
123
+ }
124
+
125
+ // Try to use pi-tui's truncate if available at runtime (optional). For compile-time we use fallback.
126
+ let truncateToWidthFn: (text: string, maxWidth: number, ellipsis?: string, pad?: boolean) => string = truncateFallback;
127
+ try {
128
+ // @ts-ignore - optional dynamic import may fail at build if module not installed; fallback is fine
129
+ // We keep fallback as default; runtime override attempted via dynamic import is not critical
130
+ } catch {}
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Simple Input fallback — mimics @earendil-works/pi-tui Input for search mode
134
+ // Handles printable chars, backspace (\x7f, \x08), delete, left/right arrows.
135
+ // Propagates focused flag per Focusable contract. Fallback when pi-tui Input not resolvable.
136
+
137
+ class SimpleSearchInput implements Component, Focusable {
138
+ value = "";
139
+ focused = false;
140
+ private cursor = 0;
141
+ // Keep for compatibility with pi-tui Input shape
142
+ getValue(): string { return this.value; }
143
+ setValue(v: string): void { this.value = v; this.cursor = v.length; }
144
+
145
+ handleInput(data: string): void {
146
+ if (!data) return;
147
+ // Handle bracketed paste markers — strip and insert content
148
+ if (data.includes("\x1b[200~")) {
149
+ const start = data.indexOf("\x1b[200~");
150
+ const end = data.indexOf("\x1b[201~");
151
+ if (end !== -1) {
152
+ const paste = data.substring(start + 6, end);
153
+ this.insertAtCursor(paste);
154
+ const remaining = data.slice(end + 6);
155
+ if (remaining) this.handleInput(remaining);
156
+ return;
157
+ } else {
158
+ // incomplete paste marker — strip marker and insert rest
159
+ data = data.replace("\x1b[200~", "");
160
+ }
161
+ }
162
+ if (data.includes("\x1b[201~")) {
163
+ data = data.replace("\x1b[201~", "");
164
+ }
165
+
166
+ // Special keys — handle one at a time if data contains escapes
167
+ // Backspace: \x7f or \x08 or \b
168
+ if (data === "\x7f" || data === "\x08" || data === "\b") {
169
+ this.handleBackspace();
170
+ return;
171
+ }
172
+ // Delete (forward): \x1b[3~
173
+ if (data === "\x1b[3~") {
174
+ this.handleDelete();
175
+ return;
176
+ }
177
+ // Arrows for cursor movement within input
178
+ if (data === "\x1b[D" || data === "\x1bOD") { // left
179
+ if (this.cursor > 0) this.cursor--;
180
+ return;
181
+ }
182
+ if (data === "\x1b[C" || data === "\x1bOC") { // right
183
+ if (this.cursor < this.value.length) this.cursor++;
184
+ return;
185
+ }
186
+ if (data === "\x1b[H" || data === "\x1bOH" || data === "\x1b[1~") { // home
187
+ this.cursor = 0;
188
+ return;
189
+ }
190
+ if (data === "\x1b[F" || data === "\x1bOF" || data === "\x1b[4~") { // end
191
+ this.cursor = this.value.length;
192
+ return;
193
+ }
194
+ // Ctrl-U (kill line)
195
+ if (data === "\x15") {
196
+ this.value = "";
197
+ this.cursor = 0;
198
+ return;
199
+ }
200
+ // Ctrl-W (delete word backwards)
201
+ if (data === "\x17") {
202
+ this.deleteWordBackwards();
203
+ return;
204
+ }
205
+ // If data looks like an escape sequence (arrow, etc.) not handled above and length >1, ignore
206
+ if (data.startsWith("\x1b") && data.length > 1) {
207
+ // Unknown escape — ignore (avoid inserting ESC into query)
208
+ return;
209
+ }
210
+ // Ctrl-C / Ctrl-D etc — ignore
211
+ if (data.length === 1 && data.charCodeAt(0) < 0x20) {
212
+ return;
213
+ }
214
+ // Printable insertion — insert at cursor (support multi-char paste without bracket markers)
215
+ // Filter out non-printable control chars
216
+ let insert = "";
217
+ for (const ch of data) {
218
+ const code = ch.charCodeAt(0);
219
+ if (code >= 0x20 && code !== 0x7f) insert += ch;
220
+ }
221
+ if (insert) this.insertAtCursor(insert);
222
+ }
223
+
224
+ private insertAtCursor(text: string): void {
225
+ this.value = this.value.slice(0, this.cursor) + text + this.value.slice(this.cursor);
226
+ this.cursor += text.length;
227
+ }
228
+ private handleBackspace(): void {
229
+ if (this.cursor > 0) {
230
+ this.value = this.value.slice(0, this.cursor - 1) + this.value.slice(this.cursor);
231
+ this.cursor--;
232
+ }
233
+ }
234
+ private handleDelete(): void {
235
+ if (this.cursor < this.value.length) {
236
+ this.value = this.value.slice(0, this.cursor) + this.value.slice(this.cursor + 1);
237
+ }
238
+ }
239
+ private deleteWordBackwards(): void {
240
+ if (this.cursor === 0) return;
241
+ let pos = this.cursor;
242
+ while (pos > 0 && this.value[pos - 1] === " ") pos--;
243
+ while (pos > 0 && this.value[pos - 1] !== " ") pos--;
244
+ this.value = this.value.slice(0, pos) + this.value.slice(this.cursor);
245
+ this.cursor = pos;
246
+ }
247
+
248
+ render(width: number): string[] {
249
+ // Render with cursor marker when focused. Use \x1b_pi:c\x07 marker for IME if needed? Simple underscore.
250
+ // Show placeholder when empty and not focused? When focused, show cursor.
251
+ const cursorMark = "\x1b_pi:c\x07";
252
+ let display: string;
253
+ if (this.focused) {
254
+ const before = this.value.slice(0, this.cursor);
255
+ const after = this.value.slice(this.cursor);
256
+ // Insert cursor marker + visual block; terminal will show cursor at marker
257
+ display = before + cursorMark + (after.length > 0 ? after : " ");
258
+ // Also truncate to width with visibleWidth handling? Keep simple truncation
259
+ // Use truncateFallback to keep ANSI safe (cursorMark is ANSI-like)
260
+ if (visibleWidthFallback(display) > width) {
261
+ // simple scroll: show window around cursor
262
+ const half = Math.floor(width / 2);
263
+ let start = Math.max(0, this.cursor - half);
264
+ // crude slice
265
+ let slice = this.value.slice(start, start + width);
266
+ const curInSlice = this.cursor - start;
267
+ display = slice.slice(0, curInSlice) + cursorMark + slice.slice(curInSlice);
268
+ }
269
+ } else {
270
+ display = this.value;
271
+ }
272
+ // Apply truncate for width
273
+ const line = truncateFallback(display, width);
274
+ return [line];
275
+ }
276
+
277
+ invalidate(): void {}
278
+ }
279
+
280
+ // ---------------------------------------------------------------------------
281
+
282
+ export interface ModelRow {
283
+ id: string; // "provider/modelId"
284
+ provider: string;
285
+ modelId: string;
286
+ visible: boolean;
287
+ name?: string;
288
+ }
289
+
290
+ export interface ModelConfViewOpts {
291
+ tui: any;
292
+ theme: any;
293
+ keybindings: any;
294
+ allModels: Array<{ provider: string; id: string; name?: string }>;
295
+ visibleSet: Set<string>;
296
+ enabledRaw: string[] | undefined;
297
+ onDone: (saved: boolean) => void;
298
+ onPersist: (nextEnabled: string[] | undefined) => Promise<void>;
299
+ allIds?: string[];
300
+ }
301
+
302
+ type ContentEntry =
303
+ | { kind: "header"; provider: string }
304
+ | { kind: "row"; row: ModelRow; flatIndex: number };
305
+
306
+ export class ModelConfView implements Component, Focusable {
307
+ private tui: any;
308
+ private theme: any;
309
+ private keybindings: any;
310
+ private onDone: (saved: boolean) => void;
311
+ private _onPersist: (next: string[] | undefined) => Promise<void>;
312
+
313
+ private rows: ModelRow[] = [];
314
+ private sortedProviders: string[] = [];
315
+ private rowsByProvider: Map<string, ModelRow[]> = new Map();
316
+ private cursorIndex: number = 0;
317
+ private collapsed: Set<string> = new Set();
318
+ private originalEnabled: string[] | undefined;
319
+ private draftEnabled: string[] | undefined;
320
+ private allIds: string[] = [];
321
+ private pendingDiscard = false;
322
+
323
+ private activeProviderIndex: number = 0;
324
+ private perProviderCursor: Map<string, number> = new Map();
325
+
326
+ private searchActive = false;
327
+ private searchQuery = "";
328
+ private filteredRows: ModelRow[] | null = null;
329
+ private searchInput: SimpleSearchInput;
330
+ private _focused = false;
331
+ get focused(): boolean { return this._focused; }
332
+ set focused(v: boolean) {
333
+ this._focused = v;
334
+ if (this.searchInput) this.searchInput.focused = v && this.searchActive;
335
+ }
336
+
337
+ private lastBulkMessage: string | null = null;
338
+ private lastBulkTimer: ReturnType<typeof setTimeout> | null = null;
339
+
340
+ constructor(opts: ModelConfViewOpts) {
341
+ this.tui = opts.tui;
342
+ this.theme = opts.theme;
343
+ this.keybindings = opts.keybindings;
344
+ this.onDone = opts.onDone;
345
+ this._onPersist = opts.onPersist;
346
+
347
+ this.searchInput = new SimpleSearchInput();
348
+ try {
349
+ const req: any = (globalThis as any).require ?? (typeof require !== "undefined" ? require : undefined);
350
+ if (req) {
351
+ let mod: any;
352
+ try { mod = req("@earendil-works/pi-tui"); } catch { try { mod = req("@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui"); } catch {} }
353
+ if (mod?.Input) {
354
+ const real = new mod.Input();
355
+ if (typeof real.value === "string" || typeof real.getValue === "function") {
356
+ const wrapped: any = real;
357
+ if (!("value" in wrapped) || typeof wrapped.value !== "string") {
358
+ Object.defineProperty(wrapped, "value", {
359
+ get() { return typeof wrapped.getValue === "function" ? wrapped.getValue() : wrapped._value ?? ""; },
360
+ set(v: string) { if (typeof wrapped.setValue === "function") wrapped.setValue(v); else wrapped._value = v; },
361
+ configurable: true,
362
+ });
363
+ }
364
+ this.searchInput = wrapped;
365
+ }
366
+ }
367
+ }
368
+ } catch {}
369
+
370
+ const all = opts.allModels ?? [];
371
+ this.rows = all.map((m) => {
372
+ const id = `${m.provider}/${m.id}`;
373
+ return {
374
+ id,
375
+ provider: m.provider,
376
+ modelId: m.id,
377
+ visible: opts.visibleSet.has(id),
378
+ name: m.name ?? m.id,
379
+ } as ModelRow;
380
+ });
381
+
382
+ this.rows.sort((a, b) => {
383
+ if (a.provider !== b.provider) return a.provider.localeCompare(b.provider);
384
+ return a.modelId.localeCompare(b.modelId);
385
+ });
386
+
387
+ this.rowsByProvider = new Map();
388
+ for (const r of this.rows) {
389
+ const arr = this.rowsByProvider.get(r.provider);
390
+ if (arr) arr.push(r);
391
+ else this.rowsByProvider.set(r.provider, [r]);
392
+ }
393
+ this.sortedProviders = [...this.rowsByProvider.keys()].sort((a, b) => a.localeCompare(b));
394
+
395
+ const ordered: ModelRow[] = [];
396
+ for (const p of this.sortedProviders) {
397
+ const grp = this.rowsByProvider.get(p)!;
398
+ grp.sort((a, b) => a.modelId.localeCompare(b.modelId));
399
+ ordered.push(...grp);
400
+ }
401
+ this.rows = ordered;
402
+
403
+ this.activeProviderIndex = 0;
404
+ this.perProviderCursor = new Map();
405
+ for (const p of this.sortedProviders) this.perProviderCursor.set(p, 0);
406
+ const initActive = this.activeProvider;
407
+ if (initActive) {
408
+ const base = this.rowsByProvider.get(initActive) ?? [];
409
+ this.cursorIndex = base.length > 0 ? 0 : -1;
410
+ } else {
411
+ this.cursorIndex = this.rows.length > 0 ? 0 : -1;
412
+ }
413
+
414
+ this.allIds = opts.allIds ?? all.map((m) => `${m.provider}/${m.id}`);
415
+ this.originalEnabled = opts.enabledRaw === undefined ? undefined : [...opts.enabledRaw];
416
+ this.draftEnabled = opts.enabledRaw === undefined ? undefined : [...opts.enabledRaw];
417
+ this.pendingDiscard = false;
418
+ }
419
+
420
+ getVisibleCount(): number {
421
+ return this.rows.filter((r) => r.visible).length;
422
+ }
423
+
424
+ private isDirty(): boolean {
425
+ const a = this.originalEnabled;
426
+ const b = this.draftEnabled;
427
+ if (a === undefined && b === undefined) return false;
428
+ if (a === undefined || b === undefined) return true;
429
+ if (a.length !== b.length) return true;
430
+ const sa = [...a].sort();
431
+ const sb = [...b].sort();
432
+ return JSON.stringify(sa) !== JSON.stringify(sb);
433
+ }
434
+
435
+ private rebuildVisibility(): void {
436
+ const draft = this.draftEnabled;
437
+ if (draft === undefined || draft.length === 0) {
438
+ for (const r of this.rows) r.visible = true;
439
+ return;
440
+ }
441
+ const hidden = draft as string[];
442
+ const VALID_THINKING = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
443
+ const stripThinking = (p: string) => {
444
+ const idx = p.lastIndexOf(":");
445
+ if (idx !== -1 && VALID_THINKING.has(p.slice(idx + 1))) return p.slice(0, idx);
446
+ return p;
447
+ };
448
+ const matchesPattern = (ref: string, pattern: string): boolean => {
449
+ const pat = stripThinking(pattern);
450
+ const lowerRef = ref.toLowerCase();
451
+ const lowerPat = pat.toLowerCase();
452
+ const bareId = ref.split("/").slice(1).join("/").toLowerCase();
453
+ const hasGlob = /[*?[\]]/.test(pat);
454
+ if (hasGlob) {
455
+ const rx = globToRegExp(pat);
456
+ if (rx.test(ref)) return true;
457
+ if (rx.test(bareId)) return true;
458
+ return false;
459
+ }
460
+ return lowerRef === lowerPat || bareId === lowerPat;
461
+ };
462
+ for (const r of this.rows) {
463
+ let isHidden = false;
464
+ for (const pat of hidden) {
465
+ if (matchesPattern(r.id, pat)) { isHidden = true; break; }
466
+ }
467
+ r.visible = !isHidden;
468
+ }
469
+ }
470
+
471
+ private safeFg(color: string, text: string): string {
472
+ try {
473
+ if (this.theme && typeof this.theme.fg === "function") {
474
+ return this.theme.fg(color, text);
475
+ }
476
+ } catch {}
477
+ return text;
478
+ }
479
+
480
+ private safeBold(text: string): string {
481
+ try {
482
+ if (this.theme && typeof this.theme.bold === "function") {
483
+ return this.theme.bold(text);
484
+ }
485
+ } catch {}
486
+ return text;
487
+ }
488
+
489
+ private safeBg(color: string, text: string): string {
490
+ try {
491
+ if (this.theme && typeof this.theme.bg === "function") {
492
+ return this.theme.bg(color, text);
493
+ }
494
+ } catch {}
495
+ return text;
496
+ }
497
+
498
+ private highlight(text: string): string {
499
+ try {
500
+ if (this.theme && typeof this.theme.bg === "function") {
501
+ try {
502
+ const v = this.theme.bg("selectedBg", text);
503
+ if (v !== text) return v;
504
+ } catch {}
505
+ try {
506
+ const v2 = (this.theme as any).bg("selection", text);
507
+ if (v2 !== text) return v2;
508
+ } catch {}
509
+ }
510
+ } catch {}
511
+ try {
512
+ if (this.theme && typeof this.theme.fg === "function") {
513
+ return this.theme.fg("accent", text);
514
+ }
515
+ } catch {}
516
+ return `› ${text}`;
517
+ }
518
+
519
+ private isProviderCollapsed(provider: string): boolean {
520
+ return this.collapsed.has(provider);
521
+ }
522
+
523
+ private get activeProvider(): string | undefined {
524
+ if (this.sortedProviders.length === 0) return undefined;
525
+ const idx = Math.max(0, Math.min(this.activeProviderIndex, this.sortedProviders.length - 1));
526
+ return this.sortedProviders[idx];
527
+ }
528
+
529
+ private get activeRows(): ModelRow[] {
530
+ const p = this.activeProvider;
531
+ if (!p) return [];
532
+ return this.rowsByProvider.get(p) ?? [];
533
+ }
534
+
535
+ private get rowsToShow(): ModelRow[] {
536
+ const base = this.activeRows;
537
+ if (this.filteredRows !== null) {
538
+ const filteredForActive = this.filteredRows.filter((r) => r.provider === this.activeProvider);
539
+ if (filteredForActive.length !== this.filteredRows.length) return filteredForActive;
540
+ return this.filteredRows;
541
+ }
542
+ return base;
543
+ }
544
+
545
+ private cursorRow(): ModelRow | undefined {
546
+ const list = this.rowsToShow;
547
+ if (this.cursorIndex < 0 || this.cursorIndex >= list.length) return undefined;
548
+ return list[this.cursorIndex];
549
+ }
550
+
551
+ private getCurrentProvider(): string | undefined {
552
+ return this.activeProvider;
553
+ }
554
+
555
+ private switchProvider(delta: number): void {
556
+ if (this.sortedProviders.length <= 1) return;
557
+ const curProvider = this.activeProvider;
558
+ if (curProvider) this.perProviderCursor.set(curProvider, this.cursorIndex);
559
+ let next = this.activeProviderIndex + delta;
560
+ next = (next + this.sortedProviders.length) % this.sortedProviders.length;
561
+ this.activeProviderIndex = next;
562
+ const nextProvider = this.activeProvider!;
563
+ let saved = this.perProviderCursor.get(nextProvider);
564
+ if (saved === undefined) saved = 0;
565
+ const targetRows = this.rowsToShowForProvider(nextProvider);
566
+ if (targetRows.length === 0) this.cursorIndex = -1;
567
+ else this.cursorIndex = Math.max(0, Math.min(saved, targetRows.length - 1));
568
+ if (this.filteredRows !== null && this.searchQuery.trim()) {
569
+ this.recomputeFilteredForActive();
570
+ const after = this.rowsToShow;
571
+ if (after.length === 0) this.cursorIndex = -1;
572
+ else this.cursorIndex = Math.max(0, Math.min(this.cursorIndex, after.length - 1));
573
+ }
574
+ }
575
+
576
+ private applyVisibleBulk(action: "include" | "exclude"): void {
577
+ const list = this.rowsToShow;
578
+ if (list.length === 0) return;
579
+ const ids = list.map((r) => `${r.provider}/${r.modelId}` as ModelId);
580
+ this.draftEnabled = applyBulk(this.draftEnabled as any, this.allIds as unknown as ModelId[], ids, action) as any;
581
+ this.rebuildVisibility();
582
+ const verb = action === "include" ? "Included" : "Excluded";
583
+ const prov = this.activeProvider ?? "";
584
+ this.lastBulkMessage = `${verb} ${ids.length} in ${prov}`;
585
+ if (this.lastBulkTimer) { try { clearTimeout(this.lastBulkTimer); } catch {} }
586
+ this.lastBulkTimer = setTimeout(() => { this.lastBulkMessage = null; try { this.tui?.requestRender?.(); } catch {} }, 4000) as any;
587
+ if (this.cursorIndex >= this.rowsToShow.length) this.cursorIndex = Math.max(0, this.rowsToShow.length - 1);
588
+ if (this.activeProvider) this.perProviderCursor.set(this.activeProvider, this.cursorIndex);
589
+ try { this.tui?.requestRender?.(); } catch {}
590
+ }
591
+
592
+ private rowsToShowForProvider(provider: string): ModelRow[] {
593
+ const base = this.rowsByProvider.get(provider) ?? [];
594
+ if (this.filteredRows !== null) {
595
+ const anyForProvider = this.filteredRows.some((r) => r.provider === provider);
596
+ if (anyForProvider) return this.filteredRows.filter((r) => r.provider === provider);
597
+ if (this.searchQuery.trim()) {
598
+ try {
599
+ return fuzzyFilter(this.searchQuery, base, (r) => `${r.provider}/${r.modelId} ${r.name ?? ""}`);
600
+ } catch { return []; }
601
+ }
602
+ return [];
603
+ }
604
+ return base;
605
+ }
606
+
607
+ private recomputeFilteredForActive(): void {
608
+ const q = this.searchQuery;
609
+ if (!q.trim()) {
610
+ this.filteredRows = null;
611
+ return;
612
+ }
613
+ const base = this.activeRows;
614
+ try {
615
+ this.filteredRows = fuzzyFilter(q, base, (r) => `${r.provider}/${r.modelId} ${r.name ?? ""}`);
616
+ } catch {
617
+ this.filteredRows = null;
618
+ }
619
+ }
620
+
621
+ private renderTabBar(width: number): string {
622
+ if (this.sortedProviders.length === 0) return this.safeFg("dim", " (no providers)");
623
+ const tabs: { styled: string; width: number }[] = [];
624
+ for (let i = 0; i < this.sortedProviders.length; i++) {
625
+ const p = this.sortedProviders[i];
626
+ const group = this.rowsByProvider.get(p) ?? [];
627
+ const vis = group.filter((r) => r.visible).length;
628
+ const total = group.length;
629
+ const count = `${vis}/${total}`;
630
+ const rawLabel = `${p} ${count}`;
631
+ let styled: string;
632
+ if (i === this.activeProviderIndex) {
633
+ const inner = ` [${rawLabel}] `;
634
+ let hl = inner;
635
+ try {
636
+ const bgged = this.safeBg("selection", inner);
637
+ if (bgged !== inner) hl = bgged;
638
+ else hl = this.safeFg("accent", this.safeBold(inner));
639
+ } catch {
640
+ hl = this.safeFg("accent", this.safeBold(inner));
641
+ }
642
+ styled = hl;
643
+ } else {
644
+ styled = this.safeFg("dim", ` [${rawLabel}] `);
645
+ }
646
+ const w = visibleWidthFallback(stripAnsi(styled));
647
+ tabs.push({ styled, width: w });
648
+ }
649
+ let totalW = tabs.reduce((s, t) => s + t.width, 0) + Math.max(0, tabs.length - 1);
650
+ if (totalW <= width) {
651
+ return tabs.map((t) => t.styled).join("");
652
+ }
653
+ let start = this.activeProviderIndex;
654
+ let end = this.activeProviderIndex + 1;
655
+ let curW = tabs[this.activeProviderIndex].width;
656
+ let left = this.activeProviderIndex - 1;
657
+ let right = this.activeProviderIndex + 1;
658
+ while (left >= 0 || right < tabs.length) {
659
+ let expanded = false;
660
+ if (left >= 0) {
661
+ const need = curW + 1 + tabs[left].width;
662
+ if (need <= width - 4) {
663
+ curW = need;
664
+ start = left;
665
+ left--;
666
+ expanded = true;
667
+ }
668
+ }
669
+ if (right < tabs.length) {
670
+ const need = curW + 1 + tabs[right].width;
671
+ if (need <= width - 4) {
672
+ curW = need;
673
+ end = right + 1;
674
+ right++;
675
+ expanded = true;
676
+ }
677
+ }
678
+ if (!expanded) break;
679
+ }
680
+ const visible = tabs.slice(start, end);
681
+ let out = visible.map((t) => t.styled).join("");
682
+ if (start > 0) out = this.safeFg("dim", "‹ ") + out;
683
+ if (end < tabs.length) out = out + this.safeFg("dim", " ›");
684
+ return this.truncate(out, width);
685
+ }
686
+
687
+ private getAvailableHeight(): number {
688
+ let h: number | undefined;
689
+ try {
690
+ const term = (this.tui as any)?.terminal;
691
+ if (term) {
692
+ if (typeof term.rows === "number") h = term.rows;
693
+ else if (typeof term.height === "number") h = term.height;
694
+ else if (typeof term.getSize === "function") {
695
+ const sz = term.getSize();
696
+ if (sz && typeof sz.rows === "number") h = sz.rows;
697
+ }
698
+ }
699
+ } catch {}
700
+ if (h === undefined || h === null) {
701
+ try {
702
+ const c: any = (globalThis as any).process?.stdout;
703
+ if (c && typeof c.rows === "number") h = c.rows;
704
+ } catch {}
705
+ }
706
+ if (h === undefined || h === null || h <= 0) h = 24;
707
+ const searchExtra = this.searchActive ? 1 : (this.filteredRows !== null && this.searchQuery ? 1 : 0);
708
+ // Half-screen: reserve half the terminal height for the list
709
+ const half = Math.floor(h / 2);
710
+ // Account for chrome: title + 2 dividers + tab bar + blank spacer + status + 2-line footer
711
+ const chrome = 8 + searchExtra;
712
+ const avail = Math.max(5, half - chrome);
713
+ return Math.min(avail, 20);
714
+ }
715
+
716
+ private truncate(text: string, width: number): string {
717
+ try {
718
+ return truncateToWidthFn(text, width);
719
+ } catch {
720
+ return truncateFallback(text, width);
721
+ }
722
+ }
723
+
724
+ render(width: number): string[] {
725
+ const lines: string[] = [];
726
+ const total = this.rows.length;
727
+ const visible = this.getVisibleCount();
728
+ const dirtyMark = this.isDirty() ? this.safeFg("warning", " ● unsaved") : "";
729
+ const titleText = this.safeFg("accent", this.safeBold("modelconf")) + this.safeFg("dim", ` ${total} models ${visible} visible`) + dirtyMark;
730
+ lines.push(this.truncate(titleText, width));
731
+ lines.push(this.truncate(this.safeFg("dim", "─".repeat(Math.max(0, width))), width));
732
+ lines.push(this.truncate(this.renderTabBar(width), width));
733
+ lines.push(this.truncate(this.safeFg("dim", "─".repeat(Math.max(0, width))), width));
734
+ if (this.searchActive) {
735
+ const inputWidth = Math.max(10, width - 4);
736
+ const rendered = this.searchInput.render(inputWidth);
737
+ const inputLine = rendered[0] ?? (this.searchInput as any).value ?? "";
738
+ const prefix = this.safeFg("accent", "/ ");
739
+ const line = prefix + inputLine;
740
+ lines.push(this.truncate(line, width));
741
+ } else if (this.filteredRows !== null && this.searchQuery) {
742
+ const hint = this.safeFg("dim", `filtered in ${this.activeProvider ?? ""}: "${this.searchQuery}" (${this.rowsToShow.length}/${this.activeRows.length}) • Esc to clear • / to search`);
743
+ lines.push(this.truncate(hint, width));
744
+ }
745
+ const rowsToShow = this.rowsToShow;
746
+ if (rowsToShow.length === 0) {
747
+ if (this.filteredRows !== null) {
748
+ const q = this.searchQuery;
749
+ const prov = this.activeProvider ?? "provider";
750
+ lines.push(this.truncate(this.safeFg("warning", ` No models match '${q}' in ${prov}`), width));
751
+ lines.push(this.truncate(this.safeFg("dim", " Press Esc to clear filter"), width));
752
+ } else if (this.sortedProviders.length === 0) {
753
+ lines.push(this.truncate(this.safeFg("dim", " No providers configured"), width));
754
+ } else {
755
+ lines.push(this.truncate(this.safeFg("dim", ` No models in ${this.activeProvider ?? "provider"}`), width));
756
+ }
757
+ } else {
758
+ const maxVisible = this.getAvailableHeight();
759
+ let start = 0;
760
+ let end = rowsToShow.length;
761
+ if (rowsToShow.length > maxVisible) {
762
+ const cursor = this.cursorIndex;
763
+ const half = Math.floor(maxVisible / 2);
764
+ start = Math.max(0, Math.min(cursor - half, rowsToShow.length - maxVisible));
765
+ end = Math.min(start + maxVisible, rowsToShow.length);
766
+ }
767
+ for (let idx = start; idx < end; idx++) {
768
+ const row = rowsToShow[idx];
769
+ const isCursor = this.cursorIndex === idx;
770
+ const mark = row.visible ? this.safeFg("success", "◉") : this.safeFg("dim", "○");
771
+ const labelPart = `${mark} ${row.modelId}`;
772
+ const dimName = row.name && row.name !== row.modelId ? this.safeFg("dim", ` ${row.name}`) : "";
773
+ let rawLine = `${labelPart}${dimName}`;
774
+ rawLine = ` ${rawLine}`;
775
+ let rendered: string;
776
+ if (isCursor) {
777
+ const highlighted = this.highlight(rawLine);
778
+ rendered = this.truncate(highlighted, width);
779
+ } else {
780
+ rendered = this.truncate(rawLine, width);
781
+ }
782
+ lines.push(rendered);
783
+ }
784
+ if (rowsToShow.length > maxVisible) {
785
+ const pos = this.cursorIndex >= 0 ? this.cursorIndex + 1 : 0;
786
+ const indicator = this.safeFg("dim", ` (${pos}/${rowsToShow.length}) in ${this.activeProvider ?? ""}`);
787
+ lines.push(this.truncate(indicator, width));
788
+ }
789
+ }
790
+ lines.push("");
791
+ if (this.pendingDiscard) {
792
+ const msg = this.safeFg("warning", "Discard changes? (y/N)") + this.safeFg("dim", " y: discard n: keep editing Esc: discard");
793
+ lines.push(this.truncate(msg, width));
794
+ } else if (this.lastBulkMessage) {
795
+ const isInc = this.lastBulkMessage.startsWith("Included") || this.lastBulkMessage.startsWith("Enabled");
796
+ const bulk = this.safeFg(isInc ? "success" : "warning", this.lastBulkMessage) + this.safeFg("dim", this.isDirty() ? " • ● unsaved — Enter save" : "");
797
+ lines.push(this.truncate(bulk, width));
798
+ } else if (this.isDirty()) {
799
+ lines.push(this.truncate(this.safeFg("warning", "● unsaved — press Enter to save"), width));
800
+ } else if (this.searchActive) {
801
+ lines.push(this.truncate(this.safeFg("dim", "filtering — Esc clear • Enter keep filter"), width));
802
+ } else {
803
+ const countLine = this.safeFg("dim", `${visible} visible / ${total} total`);
804
+ lines.push(this.truncate(countLine, width));
805
+ }
806
+ // --- Persistent keybinds footer — always visible at bottom (all binds) ---
807
+ const kb1 = "Tab/⇧Tab ←→ switch provider │ ↑↓/j k Home/End navigate │ Space toggle │ / filter";
808
+ const kb2 = "a add visible │ x hide visible │ d toggle provider │ Enter save │ Esc/q close";
809
+ const sep = this.safeFg("dim", " │ ");
810
+ // If terminal is wide enough, show in one line; otherwise split into two
811
+ const oneLine = `${kb1} │ ${kb2}`;
812
+ let footerLines: string[];
813
+ try {
814
+ const w = visibleWidthFallback(stripAnsi(oneLine));
815
+ if (w + 2 <= width) {
816
+ footerLines = [this.safeFg("dim", oneLine)];
817
+ } else {
818
+ footerLines = [this.safeFg("dim", kb1), this.safeFg("dim", kb2)];
819
+ }
820
+ } catch {
821
+ footerLines = [this.safeFg("dim", kb1), this.safeFg("dim", kb2)];
822
+ }
823
+ for (const fl of footerLines) lines.push(this.truncate(fl, width));
824
+ return lines.map((l) => this.truncate(l, width));
825
+ }
826
+
827
+ handleInput(data: string): void {
828
+ if (!data) return;
829
+ const tryKb = (keyId: string): boolean => {
830
+ try {
831
+ if (this.keybindings) {
832
+ if (typeof this.keybindings.matches === "function" && this.keybindings.matches(data, keyId)) return true;
833
+ if (typeof this.keybindings.match === "function" && this.keybindings.match(data, keyId)) return true;
834
+ }
835
+ } catch {}
836
+ return false;
837
+ };
838
+ if (this.pendingDiscard) {
839
+ if (data === "y" || data === "Y") {
840
+ this.pendingDiscard = false;
841
+ this.onDone(false);
842
+ return;
843
+ }
844
+ if (data === "n" || data === "N") {
845
+ this.pendingDiscard = false;
846
+ try { this.tui?.requestRender?.(); } catch {}
847
+ return;
848
+ }
849
+ const isEscPend = data === "\x1b" || tryKb("escape") || data.includes("\x1b[27");
850
+ if (isEscPend) {
851
+ this.pendingDiscard = false;
852
+ this.onDone(false);
853
+ return;
854
+ }
855
+ return;
856
+ }
857
+ if (!this.searchActive && data === "/") {
858
+ this.searchActive = true;
859
+ this.searchInput.focused = true;
860
+ this._focused = true;
861
+ if (typeof (this.searchInput as any).setValue === "function") (this.searchInput as any).setValue("");
862
+ else (this.searchInput as any).value = "";
863
+ this.searchQuery = "";
864
+ if (this.activeProvider) this.perProviderCursor.set(this.activeProvider, this.cursorIndex);
865
+ this.cursorIndex = this.rowsToShow.length > 0 ? 0 : -1;
866
+ try { this.tui?.requestRender?.(); } catch {}
867
+ return;
868
+ }
869
+ if (this.searchActive) {
870
+ // Arrow/Home/End navigate the filtered list while typing (left/right still edit the query).
871
+ const navUp = data === "\x1b[A" || data === "\x1bOA" || tryKb("up");
872
+ const navDown = data === "\x1b[B" || data === "\x1bOB" || tryKb("down");
873
+ const navHome = data === "\x1b[H" || data === "\x1b[1~" || data === "\x1bOH" || tryKb("home");
874
+ const navEnd = data === "\x1b[F" || data === "\x1b[4~" || data === "\x1bOF" || tryKb("end");
875
+ if (navUp || navDown || navHome || navEnd) {
876
+ const navList = this.rowsToShow;
877
+ if (navList.length > 0) {
878
+ if (navUp) this.cursorIndex = (this.cursorIndex - 1 + navList.length) % navList.length;
879
+ else if (navDown) this.cursorIndex = (this.cursorIndex + 1) % navList.length;
880
+ else if (navHome) this.cursorIndex = 0;
881
+ else this.cursorIndex = navList.length - 1;
882
+ if (this.activeProvider) this.perProviderCursor.set(this.activeProvider, this.cursorIndex);
883
+ }
884
+ try { this.tui?.requestRender?.(); } catch {}
885
+ return;
886
+ }
887
+ const isEscKitty = data.includes("\x1b[27");
888
+ const isEsc = data === "\x1b" || tryKb("escape") || isEscKitty;
889
+ const isEnter = data === "\r" || data === "\n" || data === "\r\n" || data.includes("\r") || data.includes("\n") || tryKb("enter") || tryKb("return");
890
+ if (data === "\t" || data === "\x1b[Z") {
891
+ try { this.tui?.requestRender?.(); } catch {}
892
+ return;
893
+ }
894
+ if (isEsc) {
895
+ this.searchActive = false;
896
+ this.searchInput.focused = false;
897
+ this.filteredRows = null;
898
+ this.searchQuery = "";
899
+ if (typeof (this.searchInput as any).setValue === "function") (this.searchInput as any).setValue("");
900
+ else (this.searchInput as any).value = "";
901
+ const prov = this.activeProvider;
902
+ if (prov) {
903
+ const saved = this.perProviderCursor.get(prov);
904
+ const list = this.rowsToShow;
905
+ if (list.length === 0) this.cursorIndex = -1;
906
+ else this.cursorIndex = saved !== undefined ? Math.max(0, Math.min(saved, list.length - 1)) : 0;
907
+ } else {
908
+ this.cursorIndex = this.rowsToShow.length > 0 ? 0 : -1;
909
+ }
910
+ try { this.tui?.requestRender?.(); } catch {}
911
+ return;
912
+ } else if (isEnter) {
913
+ this.searchActive = false;
914
+ this.searchInput.focused = false;
915
+ if (this.activeProvider) this.perProviderCursor.set(this.activeProvider, this.cursorIndex);
916
+ try { this.tui?.requestRender?.(); } catch {}
917
+ return;
918
+ } else {
919
+ try { (this.searchInput as any).handleInput?.(data); } catch {}
920
+ let q: string = "";
921
+ try {
922
+ const si: any = this.searchInput;
923
+ if (typeof si.getValue === "function") q = si.getValue();
924
+ else if (typeof si.value === "string") q = si.value;
925
+ else q = String(si.value ?? "");
926
+ } catch { q = (this.searchInput as any).value ?? ""; }
927
+ this.searchQuery = q;
928
+ if (!q.trim()) {
929
+ this.filteredRows = null;
930
+ const prov = this.activeProvider;
931
+ if (prov) {
932
+ const saved = this.perProviderCursor.get(prov);
933
+ this.cursorIndex = saved !== undefined ? Math.max(0, Math.min(saved, this.activeRows.length - 1)) : 0;
934
+ if (this.activeRows.length === 0) this.cursorIndex = -1;
935
+ } else {
936
+ this.cursorIndex = 0;
937
+ }
938
+ } else {
939
+ try {
940
+ const base = this.activeRows;
941
+ this.filteredRows = fuzzyFilter(q, base, (r) => `${r.provider}/${r.modelId} ${r.name ?? ""}`);
942
+ } catch {
943
+ this.filteredRows = null;
944
+ }
945
+ this.cursorIndex = this.rowsToShow.length > 0 ? 0 : -1;
946
+ }
947
+ try { this.tui?.requestRender?.(); } catch {}
948
+ return;
949
+ }
950
+ }
951
+ if (data === "\t" || data === "\x09") {
952
+ this.switchProvider(1);
953
+ try { this.tui?.requestRender?.(); } catch {}
954
+ return;
955
+ }
956
+ if (data === "\x1b[Z") {
957
+ this.switchProvider(-1);
958
+ try { this.tui?.requestRender?.(); } catch {}
959
+ return;
960
+ }
961
+ if (!this.searchActive && (data === "d" || data === "D")) {
962
+ const active = this.activeRows;
963
+ if (active.length === 0) return;
964
+ const vis = active.filter((r) => r.visible).length;
965
+ const allVisible = vis === active.length;
966
+ const ids = active.map((r) => `${r.provider}/${r.modelId}` as ModelId);
967
+ if (allVisible) {
968
+ this.draftEnabled = applyBulk(this.draftEnabled as any, this.allIds as unknown as ModelId[], ids, "exclude") as any;
969
+ this.lastBulkMessage = `Disabled all ${ids.length} in ${this.activeProvider ?? "provider"}`;
970
+ } else {
971
+ this.draftEnabled = applyBulk(this.draftEnabled as any, this.allIds as unknown as ModelId[], ids, "include") as any;
972
+ this.lastBulkMessage = `Enabled all ${ids.length} in ${this.activeProvider ?? "provider"}`;
973
+ }
974
+ this.rebuildVisibility();
975
+ if (this.lastBulkTimer) { try { clearTimeout(this.lastBulkTimer); } catch {} }
976
+ this.lastBulkTimer = setTimeout(() => { this.lastBulkMessage = null; try { this.tui?.requestRender?.(); } catch {} }, 4000) as any;
977
+ if (this.cursorIndex >= this.rowsToShow.length) this.cursorIndex = Math.max(0, this.rowsToShow.length - 1);
978
+ if (this.activeProvider) this.perProviderCursor.set(this.activeProvider, this.cursorIndex);
979
+ try { this.tui?.requestRender?.(); } catch {}
980
+ return;
981
+ }
982
+ if (!this.searchActive && (data === "a" || data === "A")) {
983
+ this.applyVisibleBulk("include");
984
+ return;
985
+ }
986
+ if (!this.searchActive && (data === "x" || data === "X")) {
987
+ this.applyVisibleBulk("exclude");
988
+ return;
989
+ }
990
+ const isEscKitty = data.includes("\x1b[27");
991
+ const isEsc = data === "\x1b" || tryKb("escape") || isEscKitty;
992
+ const isQ = data === "q" || data === "Q";
993
+ if ((isEsc || isQ) && this.filteredRows !== null) {
994
+ this.filteredRows = null;
995
+ this.searchQuery = "";
996
+ if (typeof (this.searchInput as any).setValue === "function") (this.searchInput as any).setValue("");
997
+ else (this.searchInput as any).value = "";
998
+ this.searchInput.focused = false;
999
+ this.searchActive = false;
1000
+ const prov = this.activeProvider;
1001
+ if (prov) {
1002
+ const saved = this.perProviderCursor.get(prov);
1003
+ const list = this.activeRows;
1004
+ if (list.length === 0) this.cursorIndex = -1;
1005
+ else this.cursorIndex = saved !== undefined ? Math.max(0, Math.min(saved, list.length - 1)) : 0;
1006
+ } else {
1007
+ this.cursorIndex = this.rowsToShow.length > 0 ? 0 : -1;
1008
+ }
1009
+ try { this.tui?.requestRender?.(); } catch {}
1010
+ return;
1011
+ }
1012
+ if (isEsc || isQ) {
1013
+ if (this.isDirty()) {
1014
+ this.pendingDiscard = true;
1015
+ try { this.tui?.requestRender?.(); } catch {}
1016
+ return;
1017
+ }
1018
+ this.onDone(false);
1019
+ return;
1020
+ }
1021
+ if (data === " ") {
1022
+ const row = this.cursorRow();
1023
+ if (!row) return;
1024
+ const modelId = `${row.provider}/${row.modelId}` as ModelId;
1025
+ // Hide/show operates on the full model list (this.allIds is the full registry),
1026
+ // even though the view is scoped to configured providers. This ensures toggling
1027
+ // a model affects the main full enabledModels list, not just the scoped subset.
1028
+ this.draftEnabled = toggleInEnabled(modelId, this.draftEnabled, this.allIds as ModelId[]);
1029
+ this.rebuildVisibility();
1030
+ this.pendingDiscard = false;
1031
+ if (this.activeProvider) this.perProviderCursor.set(this.activeProvider, this.cursorIndex);
1032
+ try { this.tui?.requestRender?.(); } catch {}
1033
+ return;
1034
+ }
1035
+ const isEnter = data === "\r" || data === "\n" || data === "\r\n" || tryKb("enter") || tryKb("return");
1036
+ const isEnterRaw = isEnter || data.includes("\r") || data.includes("\n");
1037
+ if (isEnterRaw) {
1038
+ const toSave = this.draftEnabled;
1039
+ Promise.resolve(this._onPersist(toSave as any)).then(() => {
1040
+ this.onDone(true);
1041
+ }).catch(() => {
1042
+ try { this.tui?.requestRender?.(); } catch {}
1043
+ });
1044
+ return;
1045
+ }
1046
+ // Left/Right also switch provider (alternative to Tab/Shift-Tab) when not in search
1047
+ const isLeft = data === "\x1b[D" || data === "\x1bOD" || tryKb("left");
1048
+ const isRight = data === "\x1b[C" || data === "\x1bOC" || tryKb("right");
1049
+ if (isLeft) {
1050
+ this.switchProvider(-1);
1051
+ try { this.tui?.requestRender?.(); } catch {}
1052
+ return;
1053
+ }
1054
+ if (isRight) {
1055
+ this.switchProvider(1);
1056
+ try { this.tui?.requestRender?.(); } catch {}
1057
+ return;
1058
+ }
1059
+ let handled = false;
1060
+ const isUp = data === "k" || data === "\x1b[A" || data === "\x1bOA" || tryKb("up") || tryKb("k");
1061
+ const isDown = data === "j" || data === "\x1b[B" || data === "\x1bOB" || tryKb("down") || tryKb("j");
1062
+ const isHome = data === "g" || data === "\x1b[H" || data === "\x1b[1~" || data === "\x1bOH" || tryKb("home") || tryKb("g");
1063
+ const isEnd = data === "G" || data === "\x1b[F" || data === "\x1b[4~" || data === "\x1bOF" || tryKb("end") || tryKb("G");
1064
+ const navList = this.rowsToShow;
1065
+ if (isUp) {
1066
+ if (navList.length > 0) {
1067
+ const next = (this.cursorIndex - 1 + navList.length) % navList.length;
1068
+ this.cursorIndex = next;
1069
+ if (this.activeProvider) this.perProviderCursor.set(this.activeProvider, this.cursorIndex);
1070
+ handled = true;
1071
+ }
1072
+ } else if (isDown) {
1073
+ if (navList.length > 0) {
1074
+ const next = (this.cursorIndex + 1) % navList.length;
1075
+ this.cursorIndex = next;
1076
+ if (this.activeProvider) this.perProviderCursor.set(this.activeProvider, this.cursorIndex);
1077
+ handled = true;
1078
+ }
1079
+ } else if (isHome) {
1080
+ if (navList.length > 0) {
1081
+ this.cursorIndex = 0;
1082
+ if (this.activeProvider) this.perProviderCursor.set(this.activeProvider, this.cursorIndex);
1083
+ handled = true;
1084
+ }
1085
+ } else if (isEnd) {
1086
+ if (navList.length > 0) {
1087
+ this.cursorIndex = navList.length - 1;
1088
+ if (this.activeProvider) this.perProviderCursor.set(this.activeProvider, this.cursorIndex);
1089
+ handled = true;
1090
+ }
1091
+ }
1092
+ if (handled) {
1093
+ try { this.tui?.requestRender?.(); } catch {}
1094
+ }
1095
+ }
1096
+
1097
+ invalidate(): void {
1098
+ try { this.tui?.requestRender?.(); } catch {}
1099
+ }
1100
+ }
1101
+