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