@selesai/code 0.10.0 → 0.10.2

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 (458) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cli/credential-print.js +4 -4
  3. package/dist/cli.js +0 -0
  4. package/dist/config.d.ts +5 -3
  5. package/dist/config.js +6 -4
  6. package/dist/core/compaction/branch-summarization.js +30 -30
  7. package/dist/core/compaction/compaction.js +81 -81
  8. package/dist/core/compaction/utils.js +2 -2
  9. package/dist/core/export-html/template.css +1066 -1066
  10. package/dist/core/export-html/template.html +55 -55
  11. package/dist/core/export-html/template.js +1864 -1864
  12. package/dist/core/export-html/vendor/highlight.min.js +1212 -1212
  13. package/dist/core/export-html/vendor/marked.min.js +78 -78
  14. package/dist/core/messages.js +7 -7
  15. package/dist/core/skills.d.ts +2 -0
  16. package/dist/core/skills.js +1 -0
  17. package/dist/extensions/context-compaction-reminder.test.ts +82 -82
  18. package/dist/extensions/context-compaction-reminder.ts +28 -28
  19. package/dist/extensions/pi-intercom/LICENSE +21 -21
  20. package/dist/extensions/pi-intercom/broker/client.test.ts +83 -83
  21. package/dist/extensions/pi-intercom/broker/framing.test.ts +114 -114
  22. package/dist/extensions/pi-intercom/broker/paths.ts +134 -134
  23. package/dist/extensions/pi-intercom/broker/runtime-claim.test.ts +34 -34
  24. package/dist/extensions/pi-intercom/broker/runtime-claim.ts +21 -21
  25. package/dist/extensions/pi-intercom/cwd.test.ts +40 -40
  26. package/dist/extensions/pi-intercom/cwd.ts +31 -31
  27. package/dist/extensions/pi-intercom/format-context.test.ts +31 -31
  28. package/dist/extensions/pi-intercom/format-context.ts +32 -32
  29. package/dist/extensions/pi-intercom/test/overlay-width.test.ts +66 -66
  30. package/dist/extensions/pi-intercom/ui/compose.ts +143 -143
  31. package/dist/extensions/pi-intercom/ui/session-list.ts +166 -166
  32. package/dist/extensions/pi-rewind-hook/README.md +245 -245
  33. package/dist/extensions/pi-rewind-hook/index.ts +1445 -1445
  34. package/dist/extensions/pi-rewind-hook/package.json +29 -29
  35. package/dist/extensions/pi-subagents/install.mjs +0 -0
  36. package/dist/extensions/pi-web-agent/package.json +31 -31
  37. package/dist/extensions/pi-web-agent/src/backends/config.ts +205 -205
  38. package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -136
  39. package/dist/extensions/pi-web-agent/src/backends/factory.ts +152 -152
  40. package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +25 -25
  41. package/dist/extensions/pi-web-agent/src/cache/ttl-cache.ts +28 -28
  42. package/dist/extensions/pi-web-agent/src/changelog-notice.ts +136 -136
  43. package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +946 -946
  44. package/dist/extensions/pi-web-agent/src/extension.ts +126 -126
  45. package/dist/extensions/pi-web-agent/src/extract/readability.ts +118 -118
  46. package/dist/extensions/pi-web-agent/src/fetch/browser-resolution.ts +199 -199
  47. package/dist/extensions/pi-web-agent/src/fetch/firecrawl-fetch.ts +100 -100
  48. package/dist/extensions/pi-web-agent/src/fetch/headless-fetch.ts +117 -117
  49. package/dist/extensions/pi-web-agent/src/fetch/http-fetch.ts +67 -67
  50. package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +60 -60
  51. package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +50 -50
  52. package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +52 -52
  53. package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +105 -105
  54. package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +45 -45
  55. package/dist/extensions/pi-web-agent/src/orchestration/index.ts +28 -28
  56. package/dist/extensions/pi-web-agent/src/orchestration/query-planner.ts +47 -47
  57. package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +376 -376
  58. package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +64 -64
  59. package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +181 -181
  60. package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +101 -101
  61. package/dist/extensions/pi-web-agent/src/orchestration/stop-decider.ts +81 -81
  62. package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +210 -210
  63. package/dist/extensions/pi-web-agent/src/presentation/config.ts +75 -75
  64. package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +61 -61
  65. package/dist/extensions/pi-web-agent/src/presentation/fetch-presentation.ts +54 -54
  66. package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +41 -41
  67. package/dist/extensions/pi-web-agent/src/presentation/select-view.ts +20 -20
  68. package/dist/extensions/pi-web-agent/src/presentation/types.ts +63 -63
  69. package/dist/extensions/pi-web-agent/src/search/brave.ts +114 -114
  70. package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +72 -72
  71. package/dist/extensions/pi-web-agent/src/search/searxng.ts +96 -96
  72. package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +71 -71
  73. package/dist/extensions/pi-web-agent/src/tools/web-fetch-headless.ts +31 -31
  74. package/dist/extensions/pi-web-agent/src/tools/web-fetch.ts +31 -31
  75. package/dist/extensions/pi-web-agent/src/tools/web-search.ts +157 -157
  76. package/dist/extensions/pi-web-agent/src/types.ts +88 -88
  77. package/dist/extensions/ponytail/package.json +8 -8
  78. package/dist/extensions/question/constants.ts +30 -30
  79. package/dist/extensions/question/helpers.ts +58 -58
  80. package/dist/extensions/question/navigation.ts +14 -14
  81. package/dist/extensions/question/package.json +19 -19
  82. package/dist/extensions/question/selection-mode.ts +46 -46
  83. package/dist/extensions/question/shortcuts.ts +44 -44
  84. package/dist/extensions/question/types.ts +132 -132
  85. package/dist/extensions/test-resolve-hook-impl.mjs +6 -6
  86. package/dist/extensions/test-resolve-hook.mjs +6 -6
  87. package/dist/extensions/web-agent-onboarding.ts +222 -222
  88. package/dist/modes/interactive/components/settings-selector.d.ts +1 -1
  89. package/dist/modes/interactive/components/startup-box.d.ts +38 -0
  90. package/dist/modes/interactive/components/startup-box.js +90 -0
  91. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  92. package/dist/modes/interactive/interactive-mode.js +72 -123
  93. package/dist/package-manager-cli.js +71 -71
  94. package/dist/rpc-entry.js +0 -0
  95. package/dist/skills/agent-browser/SKILL.md +1 -0
  96. package/dist/skills/batch-grill-me/SKILL.md +1 -0
  97. package/dist/skills/brandkit/SKILL.md +1 -0
  98. package/dist/skills/design-references/SKILL.md +1 -0
  99. package/dist/skills/design-taste-frontend/SKILL.md +1 -0
  100. package/dist/skills/design-taste-frontend-v1/SKILL.md +1 -0
  101. package/dist/skills/full-output-enforcement/SKILL.md +1 -0
  102. package/dist/skills/gpt-taste/SKILL.md +1 -0
  103. package/dist/skills/grill-me/SKILL.md +1 -0
  104. package/dist/skills/handoff/SKILL.md +1 -0
  105. package/dist/skills/handoff-text/SKILL.md +1 -0
  106. package/dist/skills/high-end-visual-design/SKILL.md +1 -0
  107. package/dist/skills/image-to-code/SKILL.md +1 -0
  108. package/dist/skills/imagegen-frontend-mobile/SKILL.md +1 -0
  109. package/dist/skills/imagegen-frontend-web/SKILL.md +1 -0
  110. package/dist/skills/implanger/SKILL.md +1 -0
  111. package/dist/skills/improve-codebase/REFERENCE.md +78 -78
  112. package/dist/skills/improve-codebase/SKILL.md +1 -0
  113. package/dist/skills/industrial-brutalist-ui/SKILL.md +1 -0
  114. package/dist/skills/minimalist-ui/SKILL.md +1 -0
  115. package/dist/skills/pi-subagents/SKILL.md +1 -0
  116. package/dist/skills/planger/SKILL.md +1 -0
  117. package/dist/skills/ponytail/SKILL.md +1 -0
  118. package/dist/skills/ponytail-audit/SKILL.md +1 -0
  119. package/dist/skills/ponytail-debt/SKILL.md +1 -0
  120. package/dist/skills/ponytail-gain/SKILL.md +1 -0
  121. package/dist/skills/ponytail-help/SKILL.md +1 -0
  122. package/dist/skills/ponytail-review/SKILL.md +1 -0
  123. package/dist/skills/redesign-existing-projects/SKILL.md +1 -0
  124. package/dist/skills/selesai-handoff/SKILL.md +1 -0
  125. package/dist/skills/stitch-design-taste/SKILL.md +1 -0
  126. package/dist/skills/unlazy/SKILL.md +1 -0
  127. package/dist/skills/web-design-guidelines/SKILL.md +1 -0
  128. package/dist/skills/workflow/SKILL.md +1 -0
  129. package/docs/compaction.md +396 -396
  130. package/docs/containerization.md +111 -111
  131. package/docs/development.md +71 -71
  132. package/docs/docs.json +164 -164
  133. package/docs/environment-variables.md +86 -86
  134. package/docs/index.md +83 -83
  135. package/docs/json.md +82 -82
  136. package/docs/models.md +502 -502
  137. package/docs/packages.md +227 -227
  138. package/docs/plans/subagent-delegation/phase-0-correctness.md +264 -264
  139. package/docs/plans/subagent-delegation/phase-1-behavioral-contract.md +485 -485
  140. package/docs/plans/subagent-delegation/phase-2-context-controls.md +281 -281
  141. package/docs/plans/subagent-delegation/phase-3-advisory-routing.md +361 -361
  142. package/docs/plans/subagent-delegation/phase-4-optional-enforcement.md +380 -380
  143. package/docs/prompt-templates.md +95 -95
  144. package/docs/providers.md +293 -293
  145. package/docs/security.md +59 -59
  146. package/docs/session-format.md +414 -414
  147. package/docs/sessions.md +145 -145
  148. package/docs/shared-host-extensions.md +109 -109
  149. package/docs/shell-aliases.md +13 -13
  150. package/docs/skills.md +231 -231
  151. package/docs/terminal-setup.md +142 -142
  152. package/docs/termux.md +127 -127
  153. package/docs/themes.md +295 -295
  154. package/docs/tmux.md +63 -63
  155. package/docs/tui.md +927 -927
  156. package/docs/windows.md +17 -17
  157. package/examples/README.md +25 -25
  158. package/examples/extensions/README.md +211 -211
  159. package/examples/extensions/auto-commit-on-exit.ts +49 -49
  160. package/examples/extensions/bash-spawn-hook.ts +30 -30
  161. package/examples/extensions/bookmark.ts +50 -50
  162. package/examples/extensions/border-status-editor.ts +150 -150
  163. package/examples/extensions/built-in-tool-renderer.ts +249 -249
  164. package/examples/extensions/claude-rules.ts +86 -86
  165. package/examples/extensions/commands.ts +72 -72
  166. package/examples/extensions/confirm-destructive.ts +59 -59
  167. package/examples/extensions/custom-compaction.ts +130 -130
  168. package/examples/extensions/custom-footer.ts +64 -64
  169. package/examples/extensions/custom-header.ts +73 -73
  170. package/examples/extensions/custom-provider-anthropic/index.ts +610 -610
  171. package/examples/extensions/custom-provider-anthropic/package-lock.json +24 -24
  172. package/examples/extensions/custom-provider-anthropic/package.json +19 -19
  173. package/examples/extensions/custom-provider-gitlab-duo/index.ts +404 -404
  174. package/examples/extensions/custom-provider-gitlab-duo/package.json +16 -16
  175. package/examples/extensions/custom-provider-gitlab-duo/test.ts +82 -82
  176. package/examples/extensions/dirty-repo-guard.ts +56 -56
  177. package/examples/extensions/doom-overlay/README.md +46 -46
  178. package/examples/extensions/doom-overlay/doom/build/doom.js +21 -21
  179. package/examples/extensions/doom-overlay/doom/build/doom.wasm +0 -0
  180. package/examples/extensions/doom-overlay/doom/build.sh +152 -152
  181. package/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +72 -72
  182. package/examples/extensions/doom-overlay/doom-component.ts +132 -132
  183. package/examples/extensions/doom-overlay/doom-engine.ts +173 -173
  184. package/examples/extensions/doom-overlay/doom-keys.ts +104 -104
  185. package/examples/extensions/doom-overlay/index.ts +74 -74
  186. package/examples/extensions/doom-overlay/wad-finder.ts +51 -51
  187. package/examples/extensions/dynamic-resources/SKILL.md +8 -8
  188. package/examples/extensions/dynamic-resources/dynamic.json +79 -79
  189. package/examples/extensions/dynamic-resources/dynamic.md +5 -5
  190. package/examples/extensions/dynamic-resources/index.ts +15 -15
  191. package/examples/extensions/dynamic-tools.ts +74 -74
  192. package/examples/extensions/event-bus.ts +43 -43
  193. package/examples/extensions/file-trigger.ts +41 -41
  194. package/examples/extensions/git-checkpoint.ts +53 -53
  195. package/examples/extensions/git-merge-and-resolve.ts +115 -115
  196. package/examples/extensions/github-issue-autocomplete.ts +185 -185
  197. package/examples/extensions/gondolin/index.ts +531 -531
  198. package/examples/extensions/gondolin/package-lock.json +185 -185
  199. package/examples/extensions/gondolin/package.json +19 -19
  200. package/examples/extensions/handoff.ts +199 -199
  201. package/examples/extensions/hello.ts +26 -26
  202. package/examples/extensions/hidden-thinking-label.ts +53 -53
  203. package/examples/extensions/inline-bash.ts +94 -94
  204. package/examples/extensions/input-transform-streaming.ts +39 -39
  205. package/examples/extensions/input-transform.ts +43 -43
  206. package/examples/extensions/interactive-shell.ts +196 -196
  207. package/examples/extensions/mac-system-theme.ts +47 -47
  208. package/examples/extensions/message-renderer.ts +59 -59
  209. package/examples/extensions/minimal-mode.ts +426 -426
  210. package/examples/extensions/modal-editor.ts +85 -85
  211. package/examples/extensions/model-status.ts +31 -31
  212. package/examples/extensions/notify.ts +55 -55
  213. package/examples/extensions/overlay-qa-tests.ts +1450 -1450
  214. package/examples/extensions/overlay-test.ts +153 -153
  215. package/examples/extensions/permission-gate.ts +34 -34
  216. package/examples/extensions/pirate.ts +47 -47
  217. package/examples/extensions/plan-mode/README.md +66 -66
  218. package/examples/extensions/plan-mode/index.ts +390 -390
  219. package/examples/extensions/plan-mode/utils.ts +168 -168
  220. package/examples/extensions/preset.ts +436 -436
  221. package/examples/extensions/project-trust.ts +64 -64
  222. package/examples/extensions/prompt-customizer.ts +97 -97
  223. package/examples/extensions/protected-paths.ts +30 -30
  224. package/examples/extensions/provider-payload.ts +18 -18
  225. package/examples/extensions/qna.ts +122 -122
  226. package/examples/extensions/question.ts +285 -285
  227. package/examples/extensions/questionnaire.ts +448 -448
  228. package/examples/extensions/rainbow-editor.ts +88 -88
  229. package/examples/extensions/reload-runtime.ts +37 -37
  230. package/examples/extensions/rpc-demo.ts +118 -118
  231. package/examples/extensions/sandbox/index.ts +321 -321
  232. package/examples/extensions/sandbox/package-lock.json +92 -92
  233. package/examples/extensions/sandbox/package.json +19 -19
  234. package/examples/extensions/send-user-message.ts +97 -97
  235. package/examples/extensions/session-name.ts +27 -27
  236. package/examples/extensions/shutdown-command.ts +63 -63
  237. package/examples/extensions/snake.ts +343 -343
  238. package/examples/extensions/space-invaders.ts +560 -560
  239. package/examples/extensions/ssh.ts +220 -220
  240. package/examples/extensions/status-line.ts +32 -32
  241. package/examples/extensions/structured-output.ts +65 -65
  242. package/examples/extensions/subagent/README.md +175 -175
  243. package/examples/extensions/subagent/agents/planner.md +37 -37
  244. package/examples/extensions/subagent/agents/reviewer.md +35 -35
  245. package/examples/extensions/subagent/agents/scout.md +50 -50
  246. package/examples/extensions/subagent/agents/worker.md +24 -24
  247. package/examples/extensions/subagent/agents.ts +126 -126
  248. package/examples/extensions/subagent/index.ts +1015 -1015
  249. package/examples/extensions/subagent/prompts/implement-and-review.md +10 -10
  250. package/examples/extensions/subagent/prompts/implement.md +10 -10
  251. package/examples/extensions/subagent/prompts/scout-and-plan.md +9 -9
  252. package/examples/extensions/summarize.ts +209 -209
  253. package/examples/extensions/system-prompt-header.ts +17 -17
  254. package/examples/extensions/tic-tac-toe.ts +1008 -1008
  255. package/examples/extensions/timed-confirm.ts +70 -70
  256. package/examples/extensions/titlebar-spinner.ts +58 -58
  257. package/examples/extensions/todo.ts +297 -297
  258. package/examples/extensions/tool-override.ts +144 -144
  259. package/examples/extensions/tools.ts +146 -146
  260. package/examples/extensions/trigger-compact.ts +50 -50
  261. package/examples/extensions/truncated-tool.ts +195 -195
  262. package/examples/extensions/widget-placement.ts +9 -9
  263. package/examples/extensions/with-deps/index.ts +32 -32
  264. package/examples/extensions/with-deps/package-lock.json +31 -31
  265. package/examples/extensions/with-deps/package.json +22 -22
  266. package/examples/extensions/working-indicator.ts +123 -123
  267. package/examples/extensions/working-message-test.ts +25 -25
  268. package/examples/rpc-extension-ui.ts +632 -632
  269. package/examples/sdk/01-minimal.ts +26 -26
  270. package/examples/sdk/02-custom-model.ts +53 -53
  271. package/examples/sdk/03-custom-prompt.ts +75 -75
  272. package/examples/sdk/04-skills.ts +55 -55
  273. package/examples/sdk/05-tools.ts +48 -48
  274. package/examples/sdk/06-extensions.ts +99 -99
  275. package/examples/sdk/07-context-files.ts +47 -47
  276. package/examples/sdk/08-prompt-templates.ts +51 -51
  277. package/examples/sdk/09-api-keys-and-oauth.ts +52 -52
  278. package/examples/sdk/10-settings.ts +53 -53
  279. package/examples/sdk/11-sessions.ts +52 -52
  280. package/examples/sdk/12-full-control.ts +79 -79
  281. package/examples/sdk/13-session-runtime.ts +67 -67
  282. package/examples/sdk/README.md +144 -144
  283. package/package.json +1 -1
  284. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/bun/cli.d.ts +0 -3
  285. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/bun/register-bedrock.d.ts +0 -2
  286. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/bun/restore-sandbox-env.d.ts +0 -17
  287. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/args.d.ts +0 -57
  288. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/config-selector.d.ts +0 -16
  289. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/file-processor.d.ts +0 -15
  290. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/initial-message.d.ts +0 -18
  291. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/list-models.d.ts +0 -9
  292. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/project-trust.d.ts +0 -10
  293. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/session-picker.d.ts +0 -10
  294. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/startup-ui.d.ts +0 -20
  295. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli.d.ts +0 -3
  296. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/config.d.ts +0 -96
  297. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/agent-session-runtime.d.ts +0 -119
  298. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/agent-session-services.d.ts +0 -85
  299. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/agent-session.d.ts +0 -620
  300. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/auth-guidance.d.ts +0 -5
  301. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/auth-storage.d.ts +0 -56
  302. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/bash-executor.d.ts +0 -32
  303. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/cache-stats.d.ts +0 -49
  304. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/branch-summarization.d.ts +0 -93
  305. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/compaction.d.ts +0 -128
  306. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/index.d.ts +0 -7
  307. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/utils.d.ts +0 -38
  308. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/defaults.d.ts +0 -3
  309. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/diagnostics.d.ts +0 -15
  310. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/event-bus.d.ts +0 -9
  311. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/exec.d.ts +0 -29
  312. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/experimental.d.ts +0 -2
  313. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/export-html/ansi-to-html.d.ts +0 -22
  314. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/export-html/index.d.ts +0 -37
  315. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/export-html/tool-renderer.d.ts +0 -34
  316. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/index.d.ts +0 -12
  317. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/loader.d.ts +0 -23
  318. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/runner.d.ts +0 -171
  319. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +0 -1261
  320. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/wrapper.d.ts +0 -20
  321. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/footer-data-provider.d.ts +0 -54
  322. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/http-dispatcher.d.ts +0 -22
  323. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/index.d.ts +0 -13
  324. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/keybindings.d.ts +0 -358
  325. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/messages.d.ts +0 -77
  326. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-config.d.ts +0 -512
  327. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-registry.d.ts +0 -43
  328. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-resolver.d.ts +0 -121
  329. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-runtime.d.ts +0 -82
  330. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/models-store.d.ts +0 -17
  331. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/output-guard.d.ts +0 -7
  332. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/package-manager.d.ts +0 -210
  333. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/project-trust.d.ts +0 -15
  334. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/prompt-templates.d.ts +0 -54
  335. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/provider-attribution.d.ts +0 -4
  336. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/provider-composer.d.ts +0 -55
  337. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/radius.d.ts +0 -2
  338. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/remote-catalog-provider.d.ts +0 -5
  339. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/resolve-config-value.d.ts +0 -30
  340. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/resource-loader.d.ts +0 -206
  341. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/runtime-credentials.d.ts +0 -15
  342. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/sdk.d.ts +0 -106
  343. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/session-cwd.d.ts +0 -19
  344. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/session-manager.d.ts +0 -356
  345. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/settings-manager.d.ts +0 -297
  346. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/skills.d.ts +0 -60
  347. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/slash-commands.d.ts +0 -15
  348. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/source-info.d.ts +0 -18
  349. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/system-prompt.d.ts +0 -28
  350. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/telemetry.d.ts +0 -3
  351. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/timings.d.ts +0 -10
  352. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/bash.d.ts +0 -68
  353. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/edit-diff.d.ts +0 -106
  354. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/edit.d.ts +0 -51
  355. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/file-mutation-queue.d.ts +0 -6
  356. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/find.d.ts +0 -35
  357. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/grep.d.ts +0 -37
  358. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/index.d.ts +0 -40
  359. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/ls.d.ts +0 -37
  360. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/output-accumulator.d.ts +0 -52
  361. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/path-utils.d.ts +0 -10
  362. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/read.d.ts +0 -35
  363. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/render-utils.d.ts +0 -24
  364. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/tool-definition-wrapper.d.ts +0 -14
  365. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/truncate.d.ts +0 -70
  366. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/write.d.ts +0 -26
  367. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/trust-manager.d.ts +0 -36
  368. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/usage-totals.d.ts +0 -19
  369. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/index.d.ts +0 -3
  370. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/client.d.ts +0 -61
  371. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/huggingface.d.ts +0 -23
  372. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/index.d.ts +0 -3
  373. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/provider.d.ts +0 -10
  374. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/ui.d.ts +0 -42
  375. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/index.d.ts +0 -35
  376. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/main.d.ts +0 -12
  377. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/migrations.d.ts +0 -33
  378. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/index.d.ts +0 -9
  379. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/armin.d.ts +0 -34
  380. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/assistant-message.d.ts +0 -22
  381. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/bash-execution.d.ts +0 -34
  382. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/bordered-loader.d.ts +0 -16
  383. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/branch-summary-message.d.ts +0 -16
  384. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/compaction-summary-message.d.ts +0 -16
  385. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/config-selector.d.ts +0 -102
  386. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/countdown-timer.d.ts +0 -14
  387. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/custom-editor.d.ts +0 -21
  388. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/custom-entry.d.ts +0 -19
  389. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/custom-message.d.ts +0 -20
  390. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/daxnuts.d.ts +0 -23
  391. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/diff.d.ts +0 -12
  392. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/dynamic-border.d.ts +0 -15
  393. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/earendil-announcement.d.ts +0 -5
  394. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/extension-editor.d.ts +0 -22
  395. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/extension-input.d.ts +0 -23
  396. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/extension-selector.d.ts +0 -26
  397. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/first-time-setup.d.ts +0 -25
  398. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/footer.d.ts +0 -32
  399. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/index.d.ts +0 -34
  400. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/keybinding-hints.d.ts +0 -13
  401. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/login-dialog.d.ts +0 -52
  402. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/model-selector.d.ts +0 -54
  403. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/oauth-selector.d.ts +0 -33
  404. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/scoped-models-selector.d.ts +0 -42
  405. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/session-selector-search.d.ts +0 -23
  406. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/session-selector.d.ts +0 -95
  407. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/settings-selector.d.ts +0 -77
  408. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/show-images-selector.d.ts +0 -10
  409. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/skill-invocation-message.d.ts +0 -17
  410. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/status-indicator.d.ts +0 -28
  411. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/theme-selector.d.ts +0 -11
  412. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/thinking-selector.d.ts +0 -11
  413. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/tool-execution.d.ts +0 -63
  414. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/tree-selector.d.ts +0 -94
  415. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/trust-selector.d.ts +0 -23
  416. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/user-message-selector.d.ts +0 -30
  417. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/user-message.d.ts +0 -14
  418. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/visual-truncate.d.ts +0 -24
  419. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/interactive-mode.d.ts +0 -394
  420. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/model-search.d.ts +0 -12
  421. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/theme/theme-controller.d.ts +0 -29
  422. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/theme/theme.d.ts +0 -120
  423. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/print-mode.d.ts +0 -28
  424. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/jsonl.d.ts +0 -17
  425. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/rpc-client.d.ts +0 -246
  426. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/rpc-mode.d.ts +0 -20
  427. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/rpc-types.d.ts +0 -457
  428. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/package-manager-cli.d.ts +0 -8
  429. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/rpc-entry.d.ts +0 -3
  430. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/ansi.d.ts +0 -2
  431. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/changelog.d.ts +0 -22
  432. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/child-process.d.ts +0 -18
  433. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/clipboard-image.d.ts +0 -11
  434. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/clipboard-native.d.ts +0 -11
  435. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/clipboard.d.ts +0 -4
  436. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/deprecation.d.ts +0 -4
  437. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/exif-orientation.d.ts +0 -5
  438. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/frontmatter.d.ts +0 -8
  439. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/fs-watch.d.ts +0 -5
  440. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/git.d.ts +0 -26
  441. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/html.d.ts +0 -7
  442. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-convert.d.ts +0 -10
  443. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-process.d.ts +0 -18
  444. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-resize-core.d.ts +0 -30
  445. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-resize-worker.d.ts +0 -2
  446. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-resize.d.ts +0 -16
  447. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/json.d.ts +0 -3
  448. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/mime.d.ts +0 -3
  449. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/open-browser.d.ts +0 -9
  450. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/paths.d.ts +0 -31
  451. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/photon.d.ts +0 -21
  452. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/pi-user-agent.d.ts +0 -2
  453. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/shell.d.ts +0 -31
  454. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/sleep.d.ts +0 -5
  455. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/syntax-highlight.d.ts +0 -12
  456. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/tools-manager.d.ts +0 -3
  457. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/version-check.d.ts +0 -15
  458. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/windows-self-update.d.ts +0 -3
package/docs/tui.md CHANGED
@@ -1,927 +1,927 @@
1
- > pi can create TUI components. Ask it to build one for your use case.
2
-
3
- # TUI Components
4
-
5
- Extensions and custom tools can render custom TUI components for interactive user interfaces. This page covers the component system and available building blocks.
6
-
7
- **Source:** [`@earendil-works/pi-tui`](https://github.com/earendil-works/pi-mono/tree/main/packages/tui)
8
-
9
- ## Component Interface
10
-
11
- All components implement:
12
-
13
- ```typescript
14
- interface Component {
15
- render(width: number): string[];
16
- handleInput?(data: string): void;
17
- wantsKeyRelease?: boolean;
18
- invalidate(): void;
19
- }
20
- ```
21
-
22
- | Method | Description |
23
- |--------|-------------|
24
- | `render(width)` | Return array of strings (one per line). Each line **must not exceed `width`**. |
25
- | `handleInput?(data)` | Receive keyboard input when component has focus. |
26
- | `wantsKeyRelease?` | If true, component receives key release events (Kitty protocol). Default: false. |
27
- | `invalidate()` | Clear cached render state. Called on theme changes. |
28
-
29
- The TUI appends a full SGR reset and OSC 8 reset at the end of each rendered line. Styles do not carry across lines. If you emit multi-line text with styling, reapply styles per line or use `wrapTextWithAnsi()` so styles are preserved for each wrapped line.
30
-
31
- ## Focusable Interface (IME Support)
32
-
33
- Components that display a text cursor and need IME (Input Method Editor) support should implement the `Focusable` interface:
34
-
35
- ```typescript
36
- import { CURSOR_MARKER, type Component, type Focusable } from "@earendil-works/pi-tui";
37
-
38
- class MyInput implements Component, Focusable {
39
- focused: boolean = false; // Set by TUI when focus changes
40
-
41
- render(width: number): string[] {
42
- const marker = this.focused ? CURSOR_MARKER : "";
43
- // Emit marker right before the fake cursor
44
- return [`> ${beforeCursor}${marker}\x1b[7m${atCursor}\x1b[27m${afterCursor}`];
45
- }
46
- }
47
- ```
48
-
49
- When a `Focusable` component has focus, TUI:
50
- 1. Sets `focused = true` on the component
51
- 2. Scans rendered output for `CURSOR_MARKER` (a zero-width APC escape sequence)
52
- 3. Positions the hardware terminal cursor at that location
53
- 4. Shows the hardware cursor only when `showHardwareCursor` is enabled
54
-
55
- The cursor remains hidden by default. This keeps the fake cursor rendering, while still positioning the hardware cursor for terminals that track IME candidate windows with hidden cursors. Some terminals require a visible hardware cursor for IME positioning; enable it with `showHardwareCursor`, `setShowHardwareCursor(true)`, or `PI_HARDWARE_CURSOR=1`. The `Editor` and `Input` built-in components already implement this interface.
56
-
57
- ### Container Components with Embedded Inputs
58
-
59
- When a container component (dialog, selector, etc.) contains an `Input` or `Editor` child, the container must implement `Focusable` and propagate the focus state to the child. Otherwise, the hardware cursor won't be positioned correctly for IME input.
60
-
61
- ```typescript
62
- import { Container, type Focusable, Input } from "@earendil-works/pi-tui";
63
-
64
- class SearchDialog extends Container implements Focusable {
65
- private searchInput: Input;
66
-
67
- // Focusable implementation - propagate to child input for IME cursor positioning
68
- private _focused = false;
69
- get focused(): boolean {
70
- return this._focused;
71
- }
72
- set focused(value: boolean) {
73
- this._focused = value;
74
- this.searchInput.focused = value;
75
- }
76
-
77
- constructor() {
78
- super();
79
- this.searchInput = new Input();
80
- this.addChild(this.searchInput);
81
- }
82
- }
83
- ```
84
-
85
- Without this propagation, typing with an IME (Chinese, Japanese, Korean, etc.) will show the candidate window in the wrong position on screen.
86
-
87
- ## Using Components
88
-
89
- **In extensions** via `ctx.ui.custom()`:
90
-
91
- ```typescript
92
- pi.on("session_start", async (_event, ctx) => {
93
- const handle = ctx.ui.custom(myComponent);
94
- // handle.requestRender() - trigger re-render
95
- // handle.close() - restore normal UI
96
- });
97
- ```
98
-
99
- **In custom tools** via `pi.ui.custom()`:
100
-
101
- ```typescript
102
- async execute(toolCallId, params, onUpdate, ctx, signal) {
103
- const handle = pi.ui.custom(myComponent);
104
- // ...
105
- handle.close();
106
- }
107
- ```
108
-
109
- ## Overlays
110
-
111
- Overlays render components on top of existing content without clearing the screen. Pass `{ overlay: true }` to `ctx.ui.custom()`:
112
-
113
- ```typescript
114
- const result = await ctx.ui.custom<string | null>(
115
- (tui, theme, keybindings, done) => new MyDialog({ onClose: done }),
116
- { overlay: true }
117
- );
118
- ```
119
-
120
- For positioning and sizing, use `overlayOptions`:
121
-
122
- ```typescript
123
- const result = await ctx.ui.custom<string | null>(
124
- (tui, theme, keybindings, done) => new SidePanel({ onClose: done }),
125
- {
126
- overlay: true,
127
- overlayOptions: {
128
- // Size: number or percentage string
129
- width: "50%", // 50% of terminal width
130
- minWidth: 40, // minimum 40 columns
131
- maxHeight: "80%", // max 80% of terminal height
132
-
133
- // Position: anchor-based (default: "center")
134
- anchor: "right-center", // 9 positions: center, top-left, top-center, etc.
135
- offsetX: -2, // offset from anchor
136
- offsetY: 0,
137
-
138
- // Or percentage/absolute positioning
139
- row: "25%", // 25% from top
140
- col: 10, // column 10
141
-
142
- // Margins
143
- margin: 2, // all sides, or { top, right, bottom, left }
144
-
145
- // Responsive: hide on narrow terminals
146
- visible: (termWidth, termHeight) => termWidth >= 80,
147
- },
148
- // Get handle for programmatic focus and visibility control
149
- onHandle: (handle) => {
150
- // handle.focus() - focus this overlay and bring it to the visual front
151
- // handle.unfocus() - release input to normal fallback
152
- // handle.unfocus({ target }) - release input to a specific component or null
153
- // handle.setHidden(true/false) - toggle visibility
154
- // handle.hide() - permanently remove
155
- },
156
- }
157
- );
158
- ```
159
-
160
- ### Overlay Focus
161
-
162
- A focused visible overlay keeps input ownership across temporary non-overlay UI. If an overlay opens another `ctx.ui.custom()` component without `{ overlay: true }`, that replacement UI receives input while it is active; when it closes, the focused overlay can reclaim input.
163
-
164
- Use `handle.unfocus()` when a visible overlay should stop owning input and let TUI fall back to another visible capturing overlay or the previous focus target. Use `handle.unfocus({ target })` when a specific component should receive input while the overlay stays visible. Passing `{ target: null }` intentionally leaves no focused component until focus is set again.
165
-
166
- ### Overlay Lifecycle
167
-
168
- Overlay components are disposed when closed. Don't reuse references - create fresh instances:
169
-
170
- ```typescript
171
- // Wrong - stale reference
172
- let menu: MenuComponent;
173
- await ctx.ui.custom((_, __, ___, done) => {
174
- menu = new MenuComponent(done);
175
- return menu;
176
- }, { overlay: true });
177
- setActiveComponent(menu); // Disposed
178
-
179
- // Correct - re-call to re-show
180
- const showMenu = () => ctx.ui.custom((_, __, ___, done) =>
181
- new MenuComponent(done), { overlay: true });
182
-
183
- await showMenu(); // First show
184
- await showMenu(); // "Back" = just call again
185
- ```
186
-
187
- See [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts) for comprehensive examples covering anchors, margins, stacking, responsive visibility, and animation.
188
-
189
- ## Built-in Components
190
-
191
- Import from `@earendil-works/pi-tui`:
192
-
193
- ```typescript
194
- import { Text, Box, Container, Spacer, Markdown } from "@earendil-works/pi-tui";
195
- ```
196
-
197
- ### Text
198
-
199
- Multi-line text with word wrapping.
200
-
201
- ```typescript
202
- const text = new Text(
203
- "Hello World", // content
204
- 1, // paddingX (default: 1)
205
- 1, // paddingY (default: 1)
206
- (s) => bgGray(s) // optional background function
207
- );
208
- text.setText("Updated");
209
- ```
210
-
211
- ### Box
212
-
213
- Container with padding and background color.
214
-
215
- ```typescript
216
- const box = new Box(
217
- 1, // paddingX
218
- 1, // paddingY
219
- (s) => bgGray(s) // background function
220
- );
221
- box.addChild(new Text("Content", 0, 0));
222
- box.setBgFn((s) => bgBlue(s));
223
- ```
224
-
225
- ### Container
226
-
227
- Groups child components vertically.
228
-
229
- ```typescript
230
- const container = new Container();
231
- container.addChild(component1);
232
- container.addChild(component2);
233
- container.removeChild(component1);
234
- ```
235
-
236
- ### Spacer
237
-
238
- Empty vertical space.
239
-
240
- ```typescript
241
- const spacer = new Spacer(2); // 2 empty lines
242
- ```
243
-
244
- ### Markdown
245
-
246
- Renders markdown with syntax highlighting.
247
-
248
- ```typescript
249
- const md = new Markdown(
250
- "# Title\n\nSome **bold** text",
251
- 1, // paddingX
252
- 1, // paddingY
253
- theme // MarkdownTheme (see below)
254
- );
255
- md.setText("Updated markdown");
256
- ```
257
-
258
- ### Image
259
-
260
- Renders images in supported terminals (Kitty, iTerm2, Ghostty, WezTerm, Warp).
261
-
262
- ```typescript
263
- const image = new Image(
264
- base64Data, // base64-encoded image
265
- "image/png", // MIME type
266
- theme, // ImageTheme
267
- { maxWidthCells: 80, maxHeightCells: 24 }
268
- );
269
- ```
270
-
271
- ## Keyboard Input
272
-
273
- Use `matchesKey()` for key detection:
274
-
275
- ```typescript
276
- import { matchesKey, Key } from "@earendil-works/pi-tui";
277
-
278
- handleInput(data: string) {
279
- if (matchesKey(data, Key.up)) {
280
- this.selectedIndex--;
281
- } else if (matchesKey(data, Key.enter)) {
282
- this.onSelect?.(this.selectedIndex);
283
- } else if (matchesKey(data, Key.escape)) {
284
- this.onCancel?.();
285
- } else if (matchesKey(data, Key.ctrl("c"))) {
286
- // Ctrl+C
287
- }
288
- }
289
- ```
290
-
291
- **Key identifiers** (use `Key.*` for autocomplete, or string literals):
292
- - Basic keys: `Key.enter`, `Key.escape`, `Key.tab`, `Key.space`, `Key.backspace`, `Key.delete`, `Key.home`, `Key.end`
293
- - Arrow keys: `Key.up`, `Key.down`, `Key.left`, `Key.right`
294
- - With modifiers: `Key.ctrl("c")`, `Key.shift("tab")`, `Key.alt("left")`, `Key.ctrlShift("p")`
295
- - String format also works: `"enter"`, `"ctrl+c"`, `"shift+tab"`, `"ctrl+shift+p"`
296
-
297
- ## Line Width
298
-
299
- **Critical:** Each line from `render()` must not exceed the `width` parameter.
300
-
301
- ```typescript
302
- import { visibleWidth, truncateToWidth } from "@earendil-works/pi-tui";
303
-
304
- render(width: number): string[] {
305
- // Truncate long lines
306
- return [truncateToWidth(this.text, width)];
307
- }
308
- ```
309
-
310
- Utilities:
311
- - `visibleWidth(str)` - Get display width (ignores ANSI codes)
312
- - `truncateToWidth(str, width, ellipsis?)` - Truncate with optional ellipsis
313
- - `wrapTextWithAnsi(str, width)` - Word wrap preserving ANSI codes
314
-
315
- ## Creating Custom Components
316
-
317
- Example: Interactive selector
318
-
319
- ```typescript
320
- import {
321
- matchesKey, Key,
322
- truncateToWidth, visibleWidth
323
- } from "@earendil-works/pi-tui";
324
-
325
- class MySelector {
326
- private items: string[];
327
- private selected = 0;
328
- private cachedWidth?: number;
329
- private cachedLines?: string[];
330
-
331
- public onSelect?: (item: string) => void;
332
- public onCancel?: () => void;
333
-
334
- constructor(items: string[]) {
335
- this.items = items;
336
- }
337
-
338
- handleInput(data: string): void {
339
- if (matchesKey(data, Key.up) && this.selected > 0) {
340
- this.selected--;
341
- this.invalidate();
342
- } else if (matchesKey(data, Key.down) && this.selected < this.items.length - 1) {
343
- this.selected++;
344
- this.invalidate();
345
- } else if (matchesKey(data, Key.enter)) {
346
- this.onSelect?.(this.items[this.selected]);
347
- } else if (matchesKey(data, Key.escape)) {
348
- this.onCancel?.();
349
- }
350
- }
351
-
352
- render(width: number): string[] {
353
- if (this.cachedLines && this.cachedWidth === width) {
354
- return this.cachedLines;
355
- }
356
-
357
- this.cachedLines = this.items.map((item, i) => {
358
- const prefix = i === this.selected ? "> " : " ";
359
- return truncateToWidth(prefix + item, width);
360
- });
361
- this.cachedWidth = width;
362
- return this.cachedLines;
363
- }
364
-
365
- invalidate(): void {
366
- this.cachedWidth = undefined;
367
- this.cachedLines = undefined;
368
- }
369
- }
370
- ```
371
-
372
- Usage in an extension:
373
-
374
- ```typescript
375
- pi.registerCommand("pick", {
376
- description: "Pick an item",
377
- handler: async (args, ctx) => {
378
- const items = ["Option A", "Option B", "Option C"];
379
- const selector = new MySelector(items);
380
-
381
- let handle: { close: () => void; requestRender: () => void };
382
-
383
- await new Promise<void>((resolve) => {
384
- selector.onSelect = (item) => {
385
- ctx.ui.notify(`Selected: ${item}`, "info");
386
- handle.close();
387
- resolve();
388
- };
389
- selector.onCancel = () => {
390
- handle.close();
391
- resolve();
392
- };
393
- handle = ctx.ui.custom(selector);
394
- });
395
- }
396
- });
397
- ```
398
-
399
- ## Theming
400
-
401
- Components accept theme objects for styling.
402
-
403
- **In `renderCall`/`renderResult`**, use the `theme` parameter:
404
-
405
- ```typescript
406
- renderResult(result, options, theme, context) {
407
- // Use theme.fg() for foreground colors
408
- return new Text(theme.fg("success", "Done!"), 0, 0);
409
-
410
- // Use theme.bg() for background colors
411
- const styled = theme.bg("toolPendingBg", theme.fg("accent", "text"));
412
- }
413
- ```
414
-
415
- **Foreground colors** (`theme.fg(color, text)`):
416
-
417
- | Category | Colors |
418
- |----------|--------|
419
- | General | `text`, `accent`, `muted`, `dim` |
420
- | Status | `success`, `error`, `warning` |
421
- | Borders | `border`, `borderAccent`, `borderMuted` |
422
- | Messages | `userMessageText`, `customMessageText`, `customMessageLabel` |
423
- | Tools | `toolTitle`, `toolOutput` |
424
- | Diffs | `toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext` |
425
- | Markdown | `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet` |
426
- | Syntax | `syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation` |
427
- | Thinking | `thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh` |
428
- | Modes | `bashMode` |
429
-
430
- **Background colors** (`theme.bg(color, text)`):
431
-
432
- `selectedBg`, `userMessageBg`, `customMessageBg`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`
433
-
434
- **For Markdown**, use `getMarkdownTheme()`:
435
-
436
- ```typescript
437
- import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
438
- import { Markdown } from "@earendil-works/pi-tui";
439
-
440
- renderResult(result, options, theme, context) {
441
- const mdTheme = getMarkdownTheme();
442
- return new Markdown(result.details.markdown, 0, 0, mdTheme);
443
- }
444
- ```
445
-
446
- **For custom components**, define your own theme interface:
447
-
448
- ```typescript
449
- interface MyTheme {
450
- selected: (s: string) => string;
451
- normal: (s: string) => string;
452
- }
453
- ```
454
-
455
- ## Debug logging
456
-
457
- Set `PI_TUI_WRITE_LOG` to capture the raw ANSI stream written to stdout.
458
-
459
- ```bash
460
- PI_TUI_WRITE_LOG=/tmp/tui-ansi.log npx tsx packages/tui/test/chat-simple.ts
461
- ```
462
-
463
- ## Performance
464
-
465
- Cache rendered output when possible:
466
-
467
- ```typescript
468
- class CachedComponent {
469
- private cachedWidth?: number;
470
- private cachedLines?: string[];
471
-
472
- render(width: number): string[] {
473
- if (this.cachedLines && this.cachedWidth === width) {
474
- return this.cachedLines;
475
- }
476
- // ... compute lines ...
477
- this.cachedWidth = width;
478
- this.cachedLines = lines;
479
- return lines;
480
- }
481
-
482
- invalidate(): void {
483
- this.cachedWidth = undefined;
484
- this.cachedLines = undefined;
485
- }
486
- }
487
- ```
488
-
489
- Call `invalidate()` when state changes, then `handle.requestRender()` to trigger re-render.
490
-
491
- ## Invalidation and Theme Changes
492
-
493
- When the theme changes, the TUI calls `invalidate()` on all components to clear their caches. Components must properly implement `invalidate()` to ensure theme changes take effect.
494
-
495
- ### The Problem
496
-
497
- If a component pre-bakes theme colors into strings (via `theme.fg()`, `theme.bg()`, etc.) and caches them, the cached strings contain ANSI escape codes from the old theme. Simply clearing the render cache isn't enough if the component stores the themed content separately.
498
-
499
- **Wrong approach** (theme colors won't update):
500
-
501
- ```typescript
502
- class BadComponent extends Container {
503
- private content: Text;
504
-
505
- constructor(message: string, theme: Theme) {
506
- super();
507
- // Pre-baked theme colors stored in Text component
508
- this.content = new Text(theme.fg("accent", message), 1, 0);
509
- this.addChild(this.content);
510
- }
511
- // No invalidate override - parent's invalidate only clears
512
- // child render caches, not the pre-baked content
513
- }
514
- ```
515
-
516
- ### The Solution
517
-
518
- Components that build content with theme colors must rebuild that content when `invalidate()` is called:
519
-
520
- ```typescript
521
- class GoodComponent extends Container {
522
- private message: string;
523
- private content: Text;
524
-
525
- constructor(message: string) {
526
- super();
527
- this.message = message;
528
- this.content = new Text("", 1, 0);
529
- this.addChild(this.content);
530
- this.updateDisplay();
531
- }
532
-
533
- private updateDisplay(): void {
534
- // Rebuild content with current theme
535
- this.content.setText(theme.fg("accent", this.message));
536
- }
537
-
538
- override invalidate(): void {
539
- super.invalidate(); // Clear child caches
540
- this.updateDisplay(); // Rebuild with new theme
541
- }
542
- }
543
- ```
544
-
545
- ### Pattern: Rebuild on Invalidate
546
-
547
- For components with complex content:
548
-
549
- ```typescript
550
- class ComplexComponent extends Container {
551
- private data: SomeData;
552
-
553
- constructor(data: SomeData) {
554
- super();
555
- this.data = data;
556
- this.rebuild();
557
- }
558
-
559
- private rebuild(): void {
560
- this.clear(); // Remove all children
561
-
562
- // Build UI with current theme
563
- this.addChild(new Text(theme.fg("accent", theme.bold("Title")), 1, 0));
564
- this.addChild(new Spacer(1));
565
-
566
- for (const item of this.data.items) {
567
- const color = item.active ? "success" : "muted";
568
- this.addChild(new Text(theme.fg(color, item.label), 1, 0));
569
- }
570
- }
571
-
572
- override invalidate(): void {
573
- super.invalidate();
574
- this.rebuild();
575
- }
576
- }
577
- ```
578
-
579
- ### When This Matters
580
-
581
- This pattern is needed when:
582
-
583
- 1. **Pre-baking theme colors** - Using `theme.fg()` or `theme.bg()` to create styled strings stored in child components
584
- 2. **Syntax highlighting** - Using `highlightCode()` which applies theme-based syntax colors
585
- 3. **Complex layouts** - Building child component trees that embed theme colors
586
-
587
- This pattern is NOT needed when:
588
-
589
- 1. **Using theme callbacks** - Passing functions like `(text) => theme.fg("accent", text)` that are called during render
590
- 2. **Simple containers** - Just grouping other components without adding themed content
591
- 3. **Stateless render** - Computing themed output fresh in every `render()` call (no caching)
592
-
593
- ## Common Patterns
594
-
595
- These patterns cover the most common UI needs in extensions. **Copy these patterns instead of building from scratch.**
596
-
597
- ### Pattern 1: Selection Dialog (SelectList)
598
-
599
- For letting users pick from a list of options. Use `SelectList` from `@earendil-works/pi-tui` with `DynamicBorder` for framing.
600
-
601
- ```typescript
602
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
603
- import { DynamicBorder } from "@earendil-works/pi-coding-agent";
604
- import { Container, type SelectItem, SelectList, Text } from "@earendil-works/pi-tui";
605
-
606
- pi.registerCommand("pick", {
607
- handler: async (_args, ctx) => {
608
- const items: SelectItem[] = [
609
- { value: "opt1", label: "Option 1", description: "First option" },
610
- { value: "opt2", label: "Option 2", description: "Second option" },
611
- { value: "opt3", label: "Option 3" }, // description is optional
612
- ];
613
-
614
- const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
615
- const container = new Container();
616
-
617
- // Top border
618
- container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
619
-
620
- // Title
621
- container.addChild(new Text(theme.fg("accent", theme.bold("Pick an Option")), 1, 0));
622
-
623
- // SelectList with theme
624
- const selectList = new SelectList(items, Math.min(items.length, 10), {
625
- selectedPrefix: (t) => theme.fg("accent", t),
626
- selectedText: (t) => theme.fg("accent", t),
627
- description: (t) => theme.fg("muted", t),
628
- scrollInfo: (t) => theme.fg("dim", t),
629
- noMatch: (t) => theme.fg("warning", t),
630
- });
631
- selectList.onSelect = (item) => done(item.value);
632
- selectList.onCancel = () => done(null);
633
- container.addChild(selectList);
634
-
635
- // Help text
636
- container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel"), 1, 0));
637
-
638
- // Bottom border
639
- container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
640
-
641
- return {
642
- render: (w) => container.render(w),
643
- invalidate: () => container.invalidate(),
644
- handleInput: (data) => { selectList.handleInput(data); tui.requestRender(); },
645
- };
646
- });
647
-
648
- if (result) {
649
- ctx.ui.notify(`Selected: ${result}`, "info");
650
- }
651
- },
652
- });
653
- ```
654
-
655
- **Examples:** [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts)
656
-
657
- ### Pattern 2: Async Operation with Cancel (BorderedLoader)
658
-
659
- For operations that take time and should be cancellable. `BorderedLoader` shows a spinner and handles escape to cancel.
660
-
661
- ```typescript
662
- import { BorderedLoader } from "@earendil-works/pi-coding-agent";
663
-
664
- pi.registerCommand("fetch", {
665
- handler: async (_args, ctx) => {
666
- const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
667
- const loader = new BorderedLoader(tui, theme, "Fetching data...");
668
- loader.onAbort = () => done(null);
669
-
670
- // Do async work
671
- fetchData(loader.signal)
672
- .then((data) => done(data))
673
- .catch(() => done(null));
674
-
675
- return loader;
676
- });
677
-
678
- if (result === null) {
679
- ctx.ui.notify("Cancelled", "info");
680
- } else {
681
- ctx.ui.setEditorText(result);
682
- }
683
- },
684
- });
685
- ```
686
-
687
- **Examples:** [qna.ts](../examples/extensions/qna.ts), [handoff.ts](../examples/extensions/handoff.ts)
688
-
689
- ### Pattern 3: Settings/Toggles (SettingsList)
690
-
691
- For toggling multiple settings. Use `SettingsList` from `@earendil-works/pi-tui` with `getSettingsListTheme()`.
692
-
693
- ```typescript
694
- import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
695
- import { Container, type SettingItem, SettingsList, Text } from "@earendil-works/pi-tui";
696
-
697
- pi.registerCommand("settings", {
698
- handler: async (_args, ctx) => {
699
- const items: SettingItem[] = [
700
- { id: "verbose", label: "Verbose mode", currentValue: "off", values: ["on", "off"] },
701
- { id: "color", label: "Color output", currentValue: "on", values: ["on", "off"] },
702
- ];
703
-
704
- await ctx.ui.custom((_tui, theme, _kb, done) => {
705
- const container = new Container();
706
- container.addChild(new Text(theme.fg("accent", theme.bold("Settings")), 1, 1));
707
-
708
- const settingsList = new SettingsList(
709
- items,
710
- Math.min(items.length + 2, 15),
711
- getSettingsListTheme(),
712
- (id, newValue) => {
713
- // Handle value change
714
- ctx.ui.notify(`${id} = ${newValue}`, "info");
715
- },
716
- () => done(undefined), // On close
717
- { enableSearch: true }, // Optional: enable fuzzy search by label
718
- );
719
- container.addChild(settingsList);
720
-
721
- return {
722
- render: (w) => container.render(w),
723
- invalidate: () => container.invalidate(),
724
- handleInput: (data) => settingsList.handleInput?.(data),
725
- };
726
- });
727
- },
728
- });
729
- ```
730
-
731
- **Examples:** [tools.ts](../examples/extensions/tools.ts)
732
-
733
- ### Pattern 4: Persistent Status Indicator
734
-
735
- Show status in the footer that persists across renders. Good for mode indicators.
736
-
737
- ```typescript
738
- // Set status (shown in footer)
739
- ctx.ui.setStatus("my-ext", ctx.ui.theme.fg("accent", "● active"));
740
-
741
- // Clear status
742
- ctx.ui.setStatus("my-ext", undefined);
743
- ```
744
-
745
- **Examples:** [status-line.ts](../examples/extensions/status-line.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts)
746
-
747
- ### Pattern 4b: Working Indicator Customization
748
-
749
- Customize the inline working indicator shown while pi is streaming a response.
750
-
751
- ```typescript
752
- // Static indicator
753
- ctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg("accent", "●")] });
754
-
755
- // Custom animated indicator
756
- ctx.ui.setWorkingIndicator({
757
- frames: [
758
- ctx.ui.theme.fg("dim", "·"),
759
- ctx.ui.theme.fg("muted", "•"),
760
- ctx.ui.theme.fg("accent", "●"),
761
- ctx.ui.theme.fg("muted", "•"),
762
- ],
763
- intervalMs: 120,
764
- });
765
-
766
- // Hide the indicator entirely
767
- ctx.ui.setWorkingIndicator({ frames: [] });
768
-
769
- // Restore pi's default spinner
770
- ctx.ui.setWorkingIndicator();
771
- ```
772
-
773
- This only affects the normal streaming working indicator. Compaction and retry loaders keep their built-in styling. Custom frames are rendered verbatim, so extensions must add their own colors when needed.
774
-
775
- **Examples:** [working-indicator.ts](../examples/extensions/working-indicator.ts)
776
-
777
- ### Pattern 5: Widgets Above/Below Editor
778
-
779
- Show persistent content above or below the input editor. Good for todo lists, progress.
780
-
781
- ```typescript
782
- // Simple string array (above editor by default)
783
- ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
784
-
785
- // Render below the editor
786
- ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"], { placement: "belowEditor" });
787
-
788
- // Or with theme
789
- ctx.ui.setWidget("my-widget", (_tui, theme) => {
790
- const lines = items.map((item, i) =>
791
- item.done
792
- ? theme.fg("success", "✓ ") + theme.fg("muted", item.text)
793
- : theme.fg("dim", "○ ") + item.text
794
- );
795
- return {
796
- render: () => lines,
797
- invalidate: () => {},
798
- };
799
- });
800
-
801
- // Clear
802
- ctx.ui.setWidget("my-widget", undefined);
803
- ```
804
-
805
- **Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)
806
-
807
- ### Pattern 6: Custom Footer
808
-
809
- Replace the footer. `footerData` exposes data not otherwise accessible to extensions.
810
-
811
- ```typescript
812
- ctx.ui.setFooter((tui, theme, footerData) => ({
813
- invalidate() {},
814
- render(width: number): string[] {
815
- // footerData.getGitBranch(): string | null
816
- // footerData.getExtensionStatuses(): ReadonlyMap<string, string>
817
- return [`${ctx.model?.id} (${footerData.getGitBranch() || "no git"})`];
818
- },
819
- dispose: footerData.onBranchChange(() => tui.requestRender()), // reactive
820
- }));
821
-
822
- ctx.ui.setFooter(undefined); // restore default
823
- ```
824
-
825
- Token stats available via `ctx.sessionManager.getBranch()` and `ctx.model`.
826
-
827
- **Examples:** [custom-footer.ts](../examples/extensions/custom-footer.ts)
828
-
829
- ### Pattern 7: Custom Editor (vim mode, etc.)
830
-
831
- Replace the main input editor with a custom implementation. Useful for modal editing (vim), different keybindings (emacs), or specialized input handling.
832
-
833
- ```typescript
834
- import { CustomEditor, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
835
- import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
836
-
837
- type Mode = "normal" | "insert";
838
-
839
- class VimEditor extends CustomEditor {
840
- private mode: Mode = "insert";
841
-
842
- handleInput(data: string): void {
843
- // Escape: switch to normal mode, or pass through for app handling
844
- if (matchesKey(data, "escape")) {
845
- if (this.mode === "insert") {
846
- this.mode = "normal";
847
- return;
848
- }
849
- // In normal mode, escape aborts agent (handled by CustomEditor)
850
- super.handleInput(data);
851
- return;
852
- }
853
-
854
- // Insert mode: pass everything to CustomEditor
855
- if (this.mode === "insert") {
856
- super.handleInput(data);
857
- return;
858
- }
859
-
860
- // Normal mode: vim-style navigation
861
- switch (data) {
862
- case "i": this.mode = "insert"; return;
863
- case "h": super.handleInput("\x1b[D"); return; // Left
864
- case "j": super.handleInput("\x1b[B"); return; // Down
865
- case "k": super.handleInput("\x1b[A"); return; // Up
866
- case "l": super.handleInput("\x1b[C"); return; // Right
867
- }
868
- // Pass unhandled keys to super (ctrl+c, etc.), but filter printable chars
869
- if (data.length === 1 && data.charCodeAt(0) >= 32) return;
870
- super.handleInput(data);
871
- }
872
-
873
- render(width: number): string[] {
874
- const lines = super.render(width);
875
- // Add mode indicator to bottom border (use truncateToWidth for ANSI-safe truncation)
876
- if (lines.length > 0) {
877
- const label = this.mode === "normal" ? " NORMAL " : " INSERT ";
878
- const lastLine = lines[lines.length - 1]!;
879
- // Pass "" as ellipsis to avoid adding "..." when truncating
880
- lines[lines.length - 1] = truncateToWidth(lastLine, width - label.length, "") + label;
881
- }
882
- return lines;
883
- }
884
- }
885
-
886
- export default function (pi: ExtensionAPI) {
887
- pi.on("session_start", (_event, ctx) => {
888
- // Factory receives theme and keybindings from the app
889
- ctx.ui.setEditorComponent((tui, theme, keybindings) =>
890
- new VimEditor(theme, keybindings)
891
- );
892
- });
893
- }
894
- ```
895
-
896
- **Key points:**
897
-
898
- - **Extend `CustomEditor`** (not base `Editor`) to get app keybindings (escape to abort, ctrl+d to exit, model switching, etc.)
899
- - **Call `super.handleInput(data)`** for keys you don't handle
900
- - **Factory pattern**: `setEditorComponent` receives a factory function that gets `tui`, `theme`, and `keybindings`
901
- - **Pass `undefined`** to restore the default editor: `ctx.ui.setEditorComponent(undefined)`
902
-
903
- **Examples:** [modal-editor.ts](../examples/extensions/modal-editor.ts)
904
-
905
- ## Key Rules
906
-
907
- 1. **Always use theme from callback** - Don't import theme directly. Use `theme` from the `ctx.ui.custom((tui, theme, keybindings, done) => ...)` callback.
908
-
909
- 2. **Always type DynamicBorder color param** - Write `(s: string) => theme.fg("accent", s)`, not `(s) => theme.fg("accent", s)`.
910
-
911
- 3. **Call tui.requestRender() after state changes** - In `handleInput`, call `tui.requestRender()` after updating state.
912
-
913
- 4. **Return the three-method object** - Custom components need `{ render, invalidate, handleInput }`.
914
-
915
- 5. **Use existing components** - `SelectList`, `SettingsList`, `BorderedLoader` cover 90% of cases. Don't rebuild them.
916
-
917
- ## Examples
918
-
919
- - **Selection UI**: [examples/extensions/preset.ts](../examples/extensions/preset.ts) - SelectList with DynamicBorder framing
920
- - **Async with cancel**: [examples/extensions/qna.ts](../examples/extensions/qna.ts) - BorderedLoader for LLM calls
921
- - **Settings toggles**: [examples/extensions/tools.ts](../examples/extensions/tools.ts) - SettingsList for tool enable/disable
922
- - **Status indicators**: [examples/extensions/plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) - setStatus and setWidget
923
- - **Working indicator**: [examples/extensions/working-indicator.ts](../examples/extensions/working-indicator.ts) - setWorkingIndicator
924
- - **Custom footer**: [examples/extensions/custom-footer.ts](../examples/extensions/custom-footer.ts) - setFooter with stats
925
- - **Custom editor**: [examples/extensions/modal-editor.ts](../examples/extensions/modal-editor.ts) - Vim-like modal editing
926
- - **Snake game**: [examples/extensions/snake.ts](../examples/extensions/snake.ts) - Full game with keyboard input, game loop
927
- - **Custom tool rendering**: [examples/extensions/todo.ts](../examples/extensions/todo.ts) - renderCall and renderResult
1
+ > pi can create TUI components. Ask it to build one for your use case.
2
+
3
+ # TUI Components
4
+
5
+ Extensions and custom tools can render custom TUI components for interactive user interfaces. This page covers the component system and available building blocks.
6
+
7
+ **Source:** [`@earendil-works/pi-tui`](https://github.com/earendil-works/pi-mono/tree/main/packages/tui)
8
+
9
+ ## Component Interface
10
+
11
+ All components implement:
12
+
13
+ ```typescript
14
+ interface Component {
15
+ render(width: number): string[];
16
+ handleInput?(data: string): void;
17
+ wantsKeyRelease?: boolean;
18
+ invalidate(): void;
19
+ }
20
+ ```
21
+
22
+ | Method | Description |
23
+ |--------|-------------|
24
+ | `render(width)` | Return array of strings (one per line). Each line **must not exceed `width`**. |
25
+ | `handleInput?(data)` | Receive keyboard input when component has focus. |
26
+ | `wantsKeyRelease?` | If true, component receives key release events (Kitty protocol). Default: false. |
27
+ | `invalidate()` | Clear cached render state. Called on theme changes. |
28
+
29
+ The TUI appends a full SGR reset and OSC 8 reset at the end of each rendered line. Styles do not carry across lines. If you emit multi-line text with styling, reapply styles per line or use `wrapTextWithAnsi()` so styles are preserved for each wrapped line.
30
+
31
+ ## Focusable Interface (IME Support)
32
+
33
+ Components that display a text cursor and need IME (Input Method Editor) support should implement the `Focusable` interface:
34
+
35
+ ```typescript
36
+ import { CURSOR_MARKER, type Component, type Focusable } from "@earendil-works/pi-tui";
37
+
38
+ class MyInput implements Component, Focusable {
39
+ focused: boolean = false; // Set by TUI when focus changes
40
+
41
+ render(width: number): string[] {
42
+ const marker = this.focused ? CURSOR_MARKER : "";
43
+ // Emit marker right before the fake cursor
44
+ return [`> ${beforeCursor}${marker}\x1b[7m${atCursor}\x1b[27m${afterCursor}`];
45
+ }
46
+ }
47
+ ```
48
+
49
+ When a `Focusable` component has focus, TUI:
50
+ 1. Sets `focused = true` on the component
51
+ 2. Scans rendered output for `CURSOR_MARKER` (a zero-width APC escape sequence)
52
+ 3. Positions the hardware terminal cursor at that location
53
+ 4. Shows the hardware cursor only when `showHardwareCursor` is enabled
54
+
55
+ The cursor remains hidden by default. This keeps the fake cursor rendering, while still positioning the hardware cursor for terminals that track IME candidate windows with hidden cursors. Some terminals require a visible hardware cursor for IME positioning; enable it with `showHardwareCursor`, `setShowHardwareCursor(true)`, or `PI_HARDWARE_CURSOR=1`. The `Editor` and `Input` built-in components already implement this interface.
56
+
57
+ ### Container Components with Embedded Inputs
58
+
59
+ When a container component (dialog, selector, etc.) contains an `Input` or `Editor` child, the container must implement `Focusable` and propagate the focus state to the child. Otherwise, the hardware cursor won't be positioned correctly for IME input.
60
+
61
+ ```typescript
62
+ import { Container, type Focusable, Input } from "@earendil-works/pi-tui";
63
+
64
+ class SearchDialog extends Container implements Focusable {
65
+ private searchInput: Input;
66
+
67
+ // Focusable implementation - propagate to child input for IME cursor positioning
68
+ private _focused = false;
69
+ get focused(): boolean {
70
+ return this._focused;
71
+ }
72
+ set focused(value: boolean) {
73
+ this._focused = value;
74
+ this.searchInput.focused = value;
75
+ }
76
+
77
+ constructor() {
78
+ super();
79
+ this.searchInput = new Input();
80
+ this.addChild(this.searchInput);
81
+ }
82
+ }
83
+ ```
84
+
85
+ Without this propagation, typing with an IME (Chinese, Japanese, Korean, etc.) will show the candidate window in the wrong position on screen.
86
+
87
+ ## Using Components
88
+
89
+ **In extensions** via `ctx.ui.custom()`:
90
+
91
+ ```typescript
92
+ pi.on("session_start", async (_event, ctx) => {
93
+ const handle = ctx.ui.custom(myComponent);
94
+ // handle.requestRender() - trigger re-render
95
+ // handle.close() - restore normal UI
96
+ });
97
+ ```
98
+
99
+ **In custom tools** via `pi.ui.custom()`:
100
+
101
+ ```typescript
102
+ async execute(toolCallId, params, onUpdate, ctx, signal) {
103
+ const handle = pi.ui.custom(myComponent);
104
+ // ...
105
+ handle.close();
106
+ }
107
+ ```
108
+
109
+ ## Overlays
110
+
111
+ Overlays render components on top of existing content without clearing the screen. Pass `{ overlay: true }` to `ctx.ui.custom()`:
112
+
113
+ ```typescript
114
+ const result = await ctx.ui.custom<string | null>(
115
+ (tui, theme, keybindings, done) => new MyDialog({ onClose: done }),
116
+ { overlay: true }
117
+ );
118
+ ```
119
+
120
+ For positioning and sizing, use `overlayOptions`:
121
+
122
+ ```typescript
123
+ const result = await ctx.ui.custom<string | null>(
124
+ (tui, theme, keybindings, done) => new SidePanel({ onClose: done }),
125
+ {
126
+ overlay: true,
127
+ overlayOptions: {
128
+ // Size: number or percentage string
129
+ width: "50%", // 50% of terminal width
130
+ minWidth: 40, // minimum 40 columns
131
+ maxHeight: "80%", // max 80% of terminal height
132
+
133
+ // Position: anchor-based (default: "center")
134
+ anchor: "right-center", // 9 positions: center, top-left, top-center, etc.
135
+ offsetX: -2, // offset from anchor
136
+ offsetY: 0,
137
+
138
+ // Or percentage/absolute positioning
139
+ row: "25%", // 25% from top
140
+ col: 10, // column 10
141
+
142
+ // Margins
143
+ margin: 2, // all sides, or { top, right, bottom, left }
144
+
145
+ // Responsive: hide on narrow terminals
146
+ visible: (termWidth, termHeight) => termWidth >= 80,
147
+ },
148
+ // Get handle for programmatic focus and visibility control
149
+ onHandle: (handle) => {
150
+ // handle.focus() - focus this overlay and bring it to the visual front
151
+ // handle.unfocus() - release input to normal fallback
152
+ // handle.unfocus({ target }) - release input to a specific component or null
153
+ // handle.setHidden(true/false) - toggle visibility
154
+ // handle.hide() - permanently remove
155
+ },
156
+ }
157
+ );
158
+ ```
159
+
160
+ ### Overlay Focus
161
+
162
+ A focused visible overlay keeps input ownership across temporary non-overlay UI. If an overlay opens another `ctx.ui.custom()` component without `{ overlay: true }`, that replacement UI receives input while it is active; when it closes, the focused overlay can reclaim input.
163
+
164
+ Use `handle.unfocus()` when a visible overlay should stop owning input and let TUI fall back to another visible capturing overlay or the previous focus target. Use `handle.unfocus({ target })` when a specific component should receive input while the overlay stays visible. Passing `{ target: null }` intentionally leaves no focused component until focus is set again.
165
+
166
+ ### Overlay Lifecycle
167
+
168
+ Overlay components are disposed when closed. Don't reuse references - create fresh instances:
169
+
170
+ ```typescript
171
+ // Wrong - stale reference
172
+ let menu: MenuComponent;
173
+ await ctx.ui.custom((_, __, ___, done) => {
174
+ menu = new MenuComponent(done);
175
+ return menu;
176
+ }, { overlay: true });
177
+ setActiveComponent(menu); // Disposed
178
+
179
+ // Correct - re-call to re-show
180
+ const showMenu = () => ctx.ui.custom((_, __, ___, done) =>
181
+ new MenuComponent(done), { overlay: true });
182
+
183
+ await showMenu(); // First show
184
+ await showMenu(); // "Back" = just call again
185
+ ```
186
+
187
+ See [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts) for comprehensive examples covering anchors, margins, stacking, responsive visibility, and animation.
188
+
189
+ ## Built-in Components
190
+
191
+ Import from `@earendil-works/pi-tui`:
192
+
193
+ ```typescript
194
+ import { Text, Box, Container, Spacer, Markdown } from "@earendil-works/pi-tui";
195
+ ```
196
+
197
+ ### Text
198
+
199
+ Multi-line text with word wrapping.
200
+
201
+ ```typescript
202
+ const text = new Text(
203
+ "Hello World", // content
204
+ 1, // paddingX (default: 1)
205
+ 1, // paddingY (default: 1)
206
+ (s) => bgGray(s) // optional background function
207
+ );
208
+ text.setText("Updated");
209
+ ```
210
+
211
+ ### Box
212
+
213
+ Container with padding and background color.
214
+
215
+ ```typescript
216
+ const box = new Box(
217
+ 1, // paddingX
218
+ 1, // paddingY
219
+ (s) => bgGray(s) // background function
220
+ );
221
+ box.addChild(new Text("Content", 0, 0));
222
+ box.setBgFn((s) => bgBlue(s));
223
+ ```
224
+
225
+ ### Container
226
+
227
+ Groups child components vertically.
228
+
229
+ ```typescript
230
+ const container = new Container();
231
+ container.addChild(component1);
232
+ container.addChild(component2);
233
+ container.removeChild(component1);
234
+ ```
235
+
236
+ ### Spacer
237
+
238
+ Empty vertical space.
239
+
240
+ ```typescript
241
+ const spacer = new Spacer(2); // 2 empty lines
242
+ ```
243
+
244
+ ### Markdown
245
+
246
+ Renders markdown with syntax highlighting.
247
+
248
+ ```typescript
249
+ const md = new Markdown(
250
+ "# Title\n\nSome **bold** text",
251
+ 1, // paddingX
252
+ 1, // paddingY
253
+ theme // MarkdownTheme (see below)
254
+ );
255
+ md.setText("Updated markdown");
256
+ ```
257
+
258
+ ### Image
259
+
260
+ Renders images in supported terminals (Kitty, iTerm2, Ghostty, WezTerm, Warp).
261
+
262
+ ```typescript
263
+ const image = new Image(
264
+ base64Data, // base64-encoded image
265
+ "image/png", // MIME type
266
+ theme, // ImageTheme
267
+ { maxWidthCells: 80, maxHeightCells: 24 }
268
+ );
269
+ ```
270
+
271
+ ## Keyboard Input
272
+
273
+ Use `matchesKey()` for key detection:
274
+
275
+ ```typescript
276
+ import { matchesKey, Key } from "@earendil-works/pi-tui";
277
+
278
+ handleInput(data: string) {
279
+ if (matchesKey(data, Key.up)) {
280
+ this.selectedIndex--;
281
+ } else if (matchesKey(data, Key.enter)) {
282
+ this.onSelect?.(this.selectedIndex);
283
+ } else if (matchesKey(data, Key.escape)) {
284
+ this.onCancel?.();
285
+ } else if (matchesKey(data, Key.ctrl("c"))) {
286
+ // Ctrl+C
287
+ }
288
+ }
289
+ ```
290
+
291
+ **Key identifiers** (use `Key.*` for autocomplete, or string literals):
292
+ - Basic keys: `Key.enter`, `Key.escape`, `Key.tab`, `Key.space`, `Key.backspace`, `Key.delete`, `Key.home`, `Key.end`
293
+ - Arrow keys: `Key.up`, `Key.down`, `Key.left`, `Key.right`
294
+ - With modifiers: `Key.ctrl("c")`, `Key.shift("tab")`, `Key.alt("left")`, `Key.ctrlShift("p")`
295
+ - String format also works: `"enter"`, `"ctrl+c"`, `"shift+tab"`, `"ctrl+shift+p"`
296
+
297
+ ## Line Width
298
+
299
+ **Critical:** Each line from `render()` must not exceed the `width` parameter.
300
+
301
+ ```typescript
302
+ import { visibleWidth, truncateToWidth } from "@earendil-works/pi-tui";
303
+
304
+ render(width: number): string[] {
305
+ // Truncate long lines
306
+ return [truncateToWidth(this.text, width)];
307
+ }
308
+ ```
309
+
310
+ Utilities:
311
+ - `visibleWidth(str)` - Get display width (ignores ANSI codes)
312
+ - `truncateToWidth(str, width, ellipsis?)` - Truncate with optional ellipsis
313
+ - `wrapTextWithAnsi(str, width)` - Word wrap preserving ANSI codes
314
+
315
+ ## Creating Custom Components
316
+
317
+ Example: Interactive selector
318
+
319
+ ```typescript
320
+ import {
321
+ matchesKey, Key,
322
+ truncateToWidth, visibleWidth
323
+ } from "@earendil-works/pi-tui";
324
+
325
+ class MySelector {
326
+ private items: string[];
327
+ private selected = 0;
328
+ private cachedWidth?: number;
329
+ private cachedLines?: string[];
330
+
331
+ public onSelect?: (item: string) => void;
332
+ public onCancel?: () => void;
333
+
334
+ constructor(items: string[]) {
335
+ this.items = items;
336
+ }
337
+
338
+ handleInput(data: string): void {
339
+ if (matchesKey(data, Key.up) && this.selected > 0) {
340
+ this.selected--;
341
+ this.invalidate();
342
+ } else if (matchesKey(data, Key.down) && this.selected < this.items.length - 1) {
343
+ this.selected++;
344
+ this.invalidate();
345
+ } else if (matchesKey(data, Key.enter)) {
346
+ this.onSelect?.(this.items[this.selected]);
347
+ } else if (matchesKey(data, Key.escape)) {
348
+ this.onCancel?.();
349
+ }
350
+ }
351
+
352
+ render(width: number): string[] {
353
+ if (this.cachedLines && this.cachedWidth === width) {
354
+ return this.cachedLines;
355
+ }
356
+
357
+ this.cachedLines = this.items.map((item, i) => {
358
+ const prefix = i === this.selected ? "> " : " ";
359
+ return truncateToWidth(prefix + item, width);
360
+ });
361
+ this.cachedWidth = width;
362
+ return this.cachedLines;
363
+ }
364
+
365
+ invalidate(): void {
366
+ this.cachedWidth = undefined;
367
+ this.cachedLines = undefined;
368
+ }
369
+ }
370
+ ```
371
+
372
+ Usage in an extension:
373
+
374
+ ```typescript
375
+ pi.registerCommand("pick", {
376
+ description: "Pick an item",
377
+ handler: async (args, ctx) => {
378
+ const items = ["Option A", "Option B", "Option C"];
379
+ const selector = new MySelector(items);
380
+
381
+ let handle: { close: () => void; requestRender: () => void };
382
+
383
+ await new Promise<void>((resolve) => {
384
+ selector.onSelect = (item) => {
385
+ ctx.ui.notify(`Selected: ${item}`, "info");
386
+ handle.close();
387
+ resolve();
388
+ };
389
+ selector.onCancel = () => {
390
+ handle.close();
391
+ resolve();
392
+ };
393
+ handle = ctx.ui.custom(selector);
394
+ });
395
+ }
396
+ });
397
+ ```
398
+
399
+ ## Theming
400
+
401
+ Components accept theme objects for styling.
402
+
403
+ **In `renderCall`/`renderResult`**, use the `theme` parameter:
404
+
405
+ ```typescript
406
+ renderResult(result, options, theme, context) {
407
+ // Use theme.fg() for foreground colors
408
+ return new Text(theme.fg("success", "Done!"), 0, 0);
409
+
410
+ // Use theme.bg() for background colors
411
+ const styled = theme.bg("toolPendingBg", theme.fg("accent", "text"));
412
+ }
413
+ ```
414
+
415
+ **Foreground colors** (`theme.fg(color, text)`):
416
+
417
+ | Category | Colors |
418
+ |----------|--------|
419
+ | General | `text`, `accent`, `muted`, `dim` |
420
+ | Status | `success`, `error`, `warning` |
421
+ | Borders | `border`, `borderAccent`, `borderMuted` |
422
+ | Messages | `userMessageText`, `customMessageText`, `customMessageLabel` |
423
+ | Tools | `toolTitle`, `toolOutput` |
424
+ | Diffs | `toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext` |
425
+ | Markdown | `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet` |
426
+ | Syntax | `syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation` |
427
+ | Thinking | `thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh` |
428
+ | Modes | `bashMode` |
429
+
430
+ **Background colors** (`theme.bg(color, text)`):
431
+
432
+ `selectedBg`, `userMessageBg`, `customMessageBg`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`
433
+
434
+ **For Markdown**, use `getMarkdownTheme()`:
435
+
436
+ ```typescript
437
+ import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
438
+ import { Markdown } from "@earendil-works/pi-tui";
439
+
440
+ renderResult(result, options, theme, context) {
441
+ const mdTheme = getMarkdownTheme();
442
+ return new Markdown(result.details.markdown, 0, 0, mdTheme);
443
+ }
444
+ ```
445
+
446
+ **For custom components**, define your own theme interface:
447
+
448
+ ```typescript
449
+ interface MyTheme {
450
+ selected: (s: string) => string;
451
+ normal: (s: string) => string;
452
+ }
453
+ ```
454
+
455
+ ## Debug logging
456
+
457
+ Set `PI_TUI_WRITE_LOG` to capture the raw ANSI stream written to stdout.
458
+
459
+ ```bash
460
+ PI_TUI_WRITE_LOG=/tmp/tui-ansi.log npx tsx packages/tui/test/chat-simple.ts
461
+ ```
462
+
463
+ ## Performance
464
+
465
+ Cache rendered output when possible:
466
+
467
+ ```typescript
468
+ class CachedComponent {
469
+ private cachedWidth?: number;
470
+ private cachedLines?: string[];
471
+
472
+ render(width: number): string[] {
473
+ if (this.cachedLines && this.cachedWidth === width) {
474
+ return this.cachedLines;
475
+ }
476
+ // ... compute lines ...
477
+ this.cachedWidth = width;
478
+ this.cachedLines = lines;
479
+ return lines;
480
+ }
481
+
482
+ invalidate(): void {
483
+ this.cachedWidth = undefined;
484
+ this.cachedLines = undefined;
485
+ }
486
+ }
487
+ ```
488
+
489
+ Call `invalidate()` when state changes, then `handle.requestRender()` to trigger re-render.
490
+
491
+ ## Invalidation and Theme Changes
492
+
493
+ When the theme changes, the TUI calls `invalidate()` on all components to clear their caches. Components must properly implement `invalidate()` to ensure theme changes take effect.
494
+
495
+ ### The Problem
496
+
497
+ If a component pre-bakes theme colors into strings (via `theme.fg()`, `theme.bg()`, etc.) and caches them, the cached strings contain ANSI escape codes from the old theme. Simply clearing the render cache isn't enough if the component stores the themed content separately.
498
+
499
+ **Wrong approach** (theme colors won't update):
500
+
501
+ ```typescript
502
+ class BadComponent extends Container {
503
+ private content: Text;
504
+
505
+ constructor(message: string, theme: Theme) {
506
+ super();
507
+ // Pre-baked theme colors stored in Text component
508
+ this.content = new Text(theme.fg("accent", message), 1, 0);
509
+ this.addChild(this.content);
510
+ }
511
+ // No invalidate override - parent's invalidate only clears
512
+ // child render caches, not the pre-baked content
513
+ }
514
+ ```
515
+
516
+ ### The Solution
517
+
518
+ Components that build content with theme colors must rebuild that content when `invalidate()` is called:
519
+
520
+ ```typescript
521
+ class GoodComponent extends Container {
522
+ private message: string;
523
+ private content: Text;
524
+
525
+ constructor(message: string) {
526
+ super();
527
+ this.message = message;
528
+ this.content = new Text("", 1, 0);
529
+ this.addChild(this.content);
530
+ this.updateDisplay();
531
+ }
532
+
533
+ private updateDisplay(): void {
534
+ // Rebuild content with current theme
535
+ this.content.setText(theme.fg("accent", this.message));
536
+ }
537
+
538
+ override invalidate(): void {
539
+ super.invalidate(); // Clear child caches
540
+ this.updateDisplay(); // Rebuild with new theme
541
+ }
542
+ }
543
+ ```
544
+
545
+ ### Pattern: Rebuild on Invalidate
546
+
547
+ For components with complex content:
548
+
549
+ ```typescript
550
+ class ComplexComponent extends Container {
551
+ private data: SomeData;
552
+
553
+ constructor(data: SomeData) {
554
+ super();
555
+ this.data = data;
556
+ this.rebuild();
557
+ }
558
+
559
+ private rebuild(): void {
560
+ this.clear(); // Remove all children
561
+
562
+ // Build UI with current theme
563
+ this.addChild(new Text(theme.fg("accent", theme.bold("Title")), 1, 0));
564
+ this.addChild(new Spacer(1));
565
+
566
+ for (const item of this.data.items) {
567
+ const color = item.active ? "success" : "muted";
568
+ this.addChild(new Text(theme.fg(color, item.label), 1, 0));
569
+ }
570
+ }
571
+
572
+ override invalidate(): void {
573
+ super.invalidate();
574
+ this.rebuild();
575
+ }
576
+ }
577
+ ```
578
+
579
+ ### When This Matters
580
+
581
+ This pattern is needed when:
582
+
583
+ 1. **Pre-baking theme colors** - Using `theme.fg()` or `theme.bg()` to create styled strings stored in child components
584
+ 2. **Syntax highlighting** - Using `highlightCode()` which applies theme-based syntax colors
585
+ 3. **Complex layouts** - Building child component trees that embed theme colors
586
+
587
+ This pattern is NOT needed when:
588
+
589
+ 1. **Using theme callbacks** - Passing functions like `(text) => theme.fg("accent", text)` that are called during render
590
+ 2. **Simple containers** - Just grouping other components without adding themed content
591
+ 3. **Stateless render** - Computing themed output fresh in every `render()` call (no caching)
592
+
593
+ ## Common Patterns
594
+
595
+ These patterns cover the most common UI needs in extensions. **Copy these patterns instead of building from scratch.**
596
+
597
+ ### Pattern 1: Selection Dialog (SelectList)
598
+
599
+ For letting users pick from a list of options. Use `SelectList` from `@earendil-works/pi-tui` with `DynamicBorder` for framing.
600
+
601
+ ```typescript
602
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
603
+ import { DynamicBorder } from "@earendil-works/pi-coding-agent";
604
+ import { Container, type SelectItem, SelectList, Text } from "@earendil-works/pi-tui";
605
+
606
+ pi.registerCommand("pick", {
607
+ handler: async (_args, ctx) => {
608
+ const items: SelectItem[] = [
609
+ { value: "opt1", label: "Option 1", description: "First option" },
610
+ { value: "opt2", label: "Option 2", description: "Second option" },
611
+ { value: "opt3", label: "Option 3" }, // description is optional
612
+ ];
613
+
614
+ const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
615
+ const container = new Container();
616
+
617
+ // Top border
618
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
619
+
620
+ // Title
621
+ container.addChild(new Text(theme.fg("accent", theme.bold("Pick an Option")), 1, 0));
622
+
623
+ // SelectList with theme
624
+ const selectList = new SelectList(items, Math.min(items.length, 10), {
625
+ selectedPrefix: (t) => theme.fg("accent", t),
626
+ selectedText: (t) => theme.fg("accent", t),
627
+ description: (t) => theme.fg("muted", t),
628
+ scrollInfo: (t) => theme.fg("dim", t),
629
+ noMatch: (t) => theme.fg("warning", t),
630
+ });
631
+ selectList.onSelect = (item) => done(item.value);
632
+ selectList.onCancel = () => done(null);
633
+ container.addChild(selectList);
634
+
635
+ // Help text
636
+ container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel"), 1, 0));
637
+
638
+ // Bottom border
639
+ container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
640
+
641
+ return {
642
+ render: (w) => container.render(w),
643
+ invalidate: () => container.invalidate(),
644
+ handleInput: (data) => { selectList.handleInput(data); tui.requestRender(); },
645
+ };
646
+ });
647
+
648
+ if (result) {
649
+ ctx.ui.notify(`Selected: ${result}`, "info");
650
+ }
651
+ },
652
+ });
653
+ ```
654
+
655
+ **Examples:** [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts)
656
+
657
+ ### Pattern 2: Async Operation with Cancel (BorderedLoader)
658
+
659
+ For operations that take time and should be cancellable. `BorderedLoader` shows a spinner and handles escape to cancel.
660
+
661
+ ```typescript
662
+ import { BorderedLoader } from "@earendil-works/pi-coding-agent";
663
+
664
+ pi.registerCommand("fetch", {
665
+ handler: async (_args, ctx) => {
666
+ const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
667
+ const loader = new BorderedLoader(tui, theme, "Fetching data...");
668
+ loader.onAbort = () => done(null);
669
+
670
+ // Do async work
671
+ fetchData(loader.signal)
672
+ .then((data) => done(data))
673
+ .catch(() => done(null));
674
+
675
+ return loader;
676
+ });
677
+
678
+ if (result === null) {
679
+ ctx.ui.notify("Cancelled", "info");
680
+ } else {
681
+ ctx.ui.setEditorText(result);
682
+ }
683
+ },
684
+ });
685
+ ```
686
+
687
+ **Examples:** [qna.ts](../examples/extensions/qna.ts), [handoff.ts](../examples/extensions/handoff.ts)
688
+
689
+ ### Pattern 3: Settings/Toggles (SettingsList)
690
+
691
+ For toggling multiple settings. Use `SettingsList` from `@earendil-works/pi-tui` with `getSettingsListTheme()`.
692
+
693
+ ```typescript
694
+ import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
695
+ import { Container, type SettingItem, SettingsList, Text } from "@earendil-works/pi-tui";
696
+
697
+ pi.registerCommand("settings", {
698
+ handler: async (_args, ctx) => {
699
+ const items: SettingItem[] = [
700
+ { id: "verbose", label: "Verbose mode", currentValue: "off", values: ["on", "off"] },
701
+ { id: "color", label: "Color output", currentValue: "on", values: ["on", "off"] },
702
+ ];
703
+
704
+ await ctx.ui.custom((_tui, theme, _kb, done) => {
705
+ const container = new Container();
706
+ container.addChild(new Text(theme.fg("accent", theme.bold("Settings")), 1, 1));
707
+
708
+ const settingsList = new SettingsList(
709
+ items,
710
+ Math.min(items.length + 2, 15),
711
+ getSettingsListTheme(),
712
+ (id, newValue) => {
713
+ // Handle value change
714
+ ctx.ui.notify(`${id} = ${newValue}`, "info");
715
+ },
716
+ () => done(undefined), // On close
717
+ { enableSearch: true }, // Optional: enable fuzzy search by label
718
+ );
719
+ container.addChild(settingsList);
720
+
721
+ return {
722
+ render: (w) => container.render(w),
723
+ invalidate: () => container.invalidate(),
724
+ handleInput: (data) => settingsList.handleInput?.(data),
725
+ };
726
+ });
727
+ },
728
+ });
729
+ ```
730
+
731
+ **Examples:** [tools.ts](../examples/extensions/tools.ts)
732
+
733
+ ### Pattern 4: Persistent Status Indicator
734
+
735
+ Show status in the footer that persists across renders. Good for mode indicators.
736
+
737
+ ```typescript
738
+ // Set status (shown in footer)
739
+ ctx.ui.setStatus("my-ext", ctx.ui.theme.fg("accent", "● active"));
740
+
741
+ // Clear status
742
+ ctx.ui.setStatus("my-ext", undefined);
743
+ ```
744
+
745
+ **Examples:** [status-line.ts](../examples/extensions/status-line.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts)
746
+
747
+ ### Pattern 4b: Working Indicator Customization
748
+
749
+ Customize the inline working indicator shown while pi is streaming a response.
750
+
751
+ ```typescript
752
+ // Static indicator
753
+ ctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg("accent", "●")] });
754
+
755
+ // Custom animated indicator
756
+ ctx.ui.setWorkingIndicator({
757
+ frames: [
758
+ ctx.ui.theme.fg("dim", "·"),
759
+ ctx.ui.theme.fg("muted", "•"),
760
+ ctx.ui.theme.fg("accent", "●"),
761
+ ctx.ui.theme.fg("muted", "•"),
762
+ ],
763
+ intervalMs: 120,
764
+ });
765
+
766
+ // Hide the indicator entirely
767
+ ctx.ui.setWorkingIndicator({ frames: [] });
768
+
769
+ // Restore pi's default spinner
770
+ ctx.ui.setWorkingIndicator();
771
+ ```
772
+
773
+ This only affects the normal streaming working indicator. Compaction and retry loaders keep their built-in styling. Custom frames are rendered verbatim, so extensions must add their own colors when needed.
774
+
775
+ **Examples:** [working-indicator.ts](../examples/extensions/working-indicator.ts)
776
+
777
+ ### Pattern 5: Widgets Above/Below Editor
778
+
779
+ Show persistent content above or below the input editor. Good for todo lists, progress.
780
+
781
+ ```typescript
782
+ // Simple string array (above editor by default)
783
+ ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
784
+
785
+ // Render below the editor
786
+ ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"], { placement: "belowEditor" });
787
+
788
+ // Or with theme
789
+ ctx.ui.setWidget("my-widget", (_tui, theme) => {
790
+ const lines = items.map((item, i) =>
791
+ item.done
792
+ ? theme.fg("success", "✓ ") + theme.fg("muted", item.text)
793
+ : theme.fg("dim", "○ ") + item.text
794
+ );
795
+ return {
796
+ render: () => lines,
797
+ invalidate: () => {},
798
+ };
799
+ });
800
+
801
+ // Clear
802
+ ctx.ui.setWidget("my-widget", undefined);
803
+ ```
804
+
805
+ **Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)
806
+
807
+ ### Pattern 6: Custom Footer
808
+
809
+ Replace the footer. `footerData` exposes data not otherwise accessible to extensions.
810
+
811
+ ```typescript
812
+ ctx.ui.setFooter((tui, theme, footerData) => ({
813
+ invalidate() {},
814
+ render(width: number): string[] {
815
+ // footerData.getGitBranch(): string | null
816
+ // footerData.getExtensionStatuses(): ReadonlyMap<string, string>
817
+ return [`${ctx.model?.id} (${footerData.getGitBranch() || "no git"})`];
818
+ },
819
+ dispose: footerData.onBranchChange(() => tui.requestRender()), // reactive
820
+ }));
821
+
822
+ ctx.ui.setFooter(undefined); // restore default
823
+ ```
824
+
825
+ Token stats available via `ctx.sessionManager.getBranch()` and `ctx.model`.
826
+
827
+ **Examples:** [custom-footer.ts](../examples/extensions/custom-footer.ts)
828
+
829
+ ### Pattern 7: Custom Editor (vim mode, etc.)
830
+
831
+ Replace the main input editor with a custom implementation. Useful for modal editing (vim), different keybindings (emacs), or specialized input handling.
832
+
833
+ ```typescript
834
+ import { CustomEditor, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
835
+ import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
836
+
837
+ type Mode = "normal" | "insert";
838
+
839
+ class VimEditor extends CustomEditor {
840
+ private mode: Mode = "insert";
841
+
842
+ handleInput(data: string): void {
843
+ // Escape: switch to normal mode, or pass through for app handling
844
+ if (matchesKey(data, "escape")) {
845
+ if (this.mode === "insert") {
846
+ this.mode = "normal";
847
+ return;
848
+ }
849
+ // In normal mode, escape aborts agent (handled by CustomEditor)
850
+ super.handleInput(data);
851
+ return;
852
+ }
853
+
854
+ // Insert mode: pass everything to CustomEditor
855
+ if (this.mode === "insert") {
856
+ super.handleInput(data);
857
+ return;
858
+ }
859
+
860
+ // Normal mode: vim-style navigation
861
+ switch (data) {
862
+ case "i": this.mode = "insert"; return;
863
+ case "h": super.handleInput("\x1b[D"); return; // Left
864
+ case "j": super.handleInput("\x1b[B"); return; // Down
865
+ case "k": super.handleInput("\x1b[A"); return; // Up
866
+ case "l": super.handleInput("\x1b[C"); return; // Right
867
+ }
868
+ // Pass unhandled keys to super (ctrl+c, etc.), but filter printable chars
869
+ if (data.length === 1 && data.charCodeAt(0) >= 32) return;
870
+ super.handleInput(data);
871
+ }
872
+
873
+ render(width: number): string[] {
874
+ const lines = super.render(width);
875
+ // Add mode indicator to bottom border (use truncateToWidth for ANSI-safe truncation)
876
+ if (lines.length > 0) {
877
+ const label = this.mode === "normal" ? " NORMAL " : " INSERT ";
878
+ const lastLine = lines[lines.length - 1]!;
879
+ // Pass "" as ellipsis to avoid adding "..." when truncating
880
+ lines[lines.length - 1] = truncateToWidth(lastLine, width - label.length, "") + label;
881
+ }
882
+ return lines;
883
+ }
884
+ }
885
+
886
+ export default function (pi: ExtensionAPI) {
887
+ pi.on("session_start", (_event, ctx) => {
888
+ // Factory receives theme and keybindings from the app
889
+ ctx.ui.setEditorComponent((tui, theme, keybindings) =>
890
+ new VimEditor(theme, keybindings)
891
+ );
892
+ });
893
+ }
894
+ ```
895
+
896
+ **Key points:**
897
+
898
+ - **Extend `CustomEditor`** (not base `Editor`) to get app keybindings (escape to abort, ctrl+d to exit, model switching, etc.)
899
+ - **Call `super.handleInput(data)`** for keys you don't handle
900
+ - **Factory pattern**: `setEditorComponent` receives a factory function that gets `tui`, `theme`, and `keybindings`
901
+ - **Pass `undefined`** to restore the default editor: `ctx.ui.setEditorComponent(undefined)`
902
+
903
+ **Examples:** [modal-editor.ts](../examples/extensions/modal-editor.ts)
904
+
905
+ ## Key Rules
906
+
907
+ 1. **Always use theme from callback** - Don't import theme directly. Use `theme` from the `ctx.ui.custom((tui, theme, keybindings, done) => ...)` callback.
908
+
909
+ 2. **Always type DynamicBorder color param** - Write `(s: string) => theme.fg("accent", s)`, not `(s) => theme.fg("accent", s)`.
910
+
911
+ 3. **Call tui.requestRender() after state changes** - In `handleInput`, call `tui.requestRender()` after updating state.
912
+
913
+ 4. **Return the three-method object** - Custom components need `{ render, invalidate, handleInput }`.
914
+
915
+ 5. **Use existing components** - `SelectList`, `SettingsList`, `BorderedLoader` cover 90% of cases. Don't rebuild them.
916
+
917
+ ## Examples
918
+
919
+ - **Selection UI**: [examples/extensions/preset.ts](../examples/extensions/preset.ts) - SelectList with DynamicBorder framing
920
+ - **Async with cancel**: [examples/extensions/qna.ts](../examples/extensions/qna.ts) - BorderedLoader for LLM calls
921
+ - **Settings toggles**: [examples/extensions/tools.ts](../examples/extensions/tools.ts) - SettingsList for tool enable/disable
922
+ - **Status indicators**: [examples/extensions/plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) - setStatus and setWidget
923
+ - **Working indicator**: [examples/extensions/working-indicator.ts](../examples/extensions/working-indicator.ts) - setWorkingIndicator
924
+ - **Custom footer**: [examples/extensions/custom-footer.ts](../examples/extensions/custom-footer.ts) - setFooter with stats
925
+ - **Custom editor**: [examples/extensions/modal-editor.ts](../examples/extensions/modal-editor.ts) - Vim-like modal editing
926
+ - **Snake game**: [examples/extensions/snake.ts](../examples/extensions/snake.ts) - Full game with keyboard input, game loop
927
+ - **Custom tool rendering**: [examples/extensions/todo.ts](../examples/extensions/todo.ts) - renderCall and renderResult