painapple-code 1.0.0__py3-none-any.whl

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 (436) hide show
  1. painapple_code/__init__.py +30 -0
  2. painapple_code/__main__.py +18 -0
  3. painapple_code/_version.py +24 -0
  4. painapple_code/auth_middleware.py +930 -0
  5. painapple_code/cli/__init__.py +204 -0
  6. painapple_code/cli/deploy/__init__.py +15 -0
  7. painapple_code/cli/deploy/claude_seed.py +41 -0
  8. painapple_code/cli/deploy/config.py +260 -0
  9. painapple_code/cli/deploy/container.py +715 -0
  10. painapple_code/cli/deploy/launch.py +163 -0
  11. painapple_code/cli/deploy/runtime.py +223 -0
  12. painapple_code/cli/lifecycle_cmd.py +443 -0
  13. painapple_code/cli/list_cmd.py +572 -0
  14. painapple_code/cli/manage_cmd.py +670 -0
  15. painapple_code/cli/netinfo.py +125 -0
  16. painapple_code/cli/profiles.py +390 -0
  17. painapple_code/cli/serve_args.py +163 -0
  18. painapple_code/cli/serve_config.py +256 -0
  19. painapple_code/cli/setup_cmd.py +664 -0
  20. painapple_code/cli/ui.py +438 -0
  21. painapple_code/cost_analytics.py +879 -0
  22. painapple_code/data/models.defaults.yaml +18 -0
  23. painapple_code/data/models.yaml +18 -0
  24. painapple_code/data/presets.defaults.json +32 -0
  25. painapple_code/data/strings.yaml +2269 -0
  26. painapple_code/helpers.py +406 -0
  27. painapple_code/paths.py +1514 -0
  28. painapple_code/prompt_explorer.py +984 -0
  29. painapple_code/providers/__init__.py +224 -0
  30. painapple_code/providers/base.py +710 -0
  31. painapple_code/providers/claude/__init__.py +74 -0
  32. painapple_code/providers/claude/capabilities.py +218 -0
  33. painapple_code/providers/claude/errors.py +79 -0
  34. painapple_code/providers/claude/launch.py +50 -0
  35. painapple_code/providers/claude/summary.py +89 -0
  36. painapple_code/providers/claude/translate.py +74 -0
  37. painapple_code/providers/claude_sdk/__init__.py +139 -0
  38. painapple_code/providers/claude_sdk/driver.py +412 -0
  39. painapple_code/providers/codex/__init__.py +87 -0
  40. painapple_code/providers/codex/capabilities.py +321 -0
  41. painapple_code/providers/codex/errors.py +70 -0
  42. painapple_code/providers/codex/launch.py +142 -0
  43. painapple_code/providers/codex/summary.py +196 -0
  44. painapple_code/providers/codex/translate.py +204 -0
  45. painapple_code/providers/codex_app_server/__init__.py +100 -0
  46. painapple_code/providers/codex_app_server/capabilities.py +41 -0
  47. painapple_code/providers/codex_app_server/launch.py +202 -0
  48. painapple_code/providers/codex_app_server/summary.py +291 -0
  49. painapple_code/providers/codex_app_server/translate.py +263 -0
  50. painapple_code/providers/codex_app_server/transport.py +238 -0
  51. painapple_code/routes/__init__.py +0 -0
  52. painapple_code/routes/api_agents.py +441 -0
  53. painapple_code/routes/api_app.py +368 -0
  54. painapple_code/routes/api_app_commit_sections.py +127 -0
  55. painapple_code/routes/api_app_config.py +997 -0
  56. painapple_code/routes/api_app_session_prefs.py +217 -0
  57. painapple_code/routes/api_browser.py +690 -0
  58. painapple_code/routes/api_commands.py +374 -0
  59. painapple_code/routes/api_costs.py +128 -0
  60. painapple_code/routes/api_drafts.py +139 -0
  61. painapple_code/routes/api_exec.py +149 -0
  62. painapple_code/routes/api_files.py +542 -0
  63. painapple_code/routes/api_git.py +657 -0
  64. painapple_code/routes/api_logs.py +649 -0
  65. painapple_code/routes/api_plugins.py +275 -0
  66. painapple_code/routes/api_project_config.py +418 -0
  67. painapple_code/routes/api_prompts.py +231 -0
  68. painapple_code/routes/api_providers.py +53 -0
  69. painapple_code/routes/api_search.py +411 -0
  70. painapple_code/routes/api_session_stash.py +234 -0
  71. painapple_code/routes/api_session_welcome.py +151 -0
  72. painapple_code/routes/api_sessions.py +742 -0
  73. painapple_code/routes/api_shadow.py +323 -0
  74. painapple_code/routes/api_shadow_db.py +512 -0
  75. painapple_code/routes/api_shadow_files.py +332 -0
  76. painapple_code/routes/api_shadow_search.py +476 -0
  77. painapple_code/routes/api_skills.py +610 -0
  78. painapple_code/routes/api_tasks.py +147 -0
  79. painapple_code/routes/api_terminal.py +540 -0
  80. painapple_code/routes/api_upload.py +293 -0
  81. painapple_code/routes/api_viewer.py +229 -0
  82. painapple_code/routes/dependencies.py +109 -0
  83. painapple_code/routes/ws_chat.py +837 -0
  84. painapple_code/server.py +2208 -0
  85. painapple_code/server_logging.py +263 -0
  86. painapple_code/services/__init__.py +0 -0
  87. painapple_code/services/agent_session.py +2738 -0
  88. painapple_code/session_store.py +253 -0
  89. painapple_code/session_store_core.py +879 -0
  90. painapple_code/shadow_db.py +830 -0
  91. painapple_code/shadow_db_plans.py +369 -0
  92. painapple_code/shadow_db_queries.py +595 -0
  93. painapple_code/shadow_db_schema.py +676 -0
  94. painapple_code/shadow_git.py +1296 -0
  95. painapple_code/shadow_git_frontmatter.py +314 -0
  96. painapple_code/shadow_git_sections.py +520 -0
  97. painapple_code/shadow_git_summary.py +462 -0
  98. painapple_code/shadow_parser.py +560 -0
  99. painapple_code/static/css/00-variables.css +276 -0
  100. painapple_code/static/css/01-base.css +245 -0
  101. painapple_code/static/css/02-tooltips.css +36 -0
  102. painapple_code/static/css/09-left-rail.css +448 -0
  103. painapple_code/static/css/10-header.css +777 -0
  104. painapple_code/static/css/11-connection-bar.css +120 -0
  105. painapple_code/static/css/20-messages.css +750 -0
  106. painapple_code/static/css/21-tool-blocks.css +2340 -0
  107. painapple_code/static/css/22-edit-diff.css +619 -0
  108. painapple_code/static/css/23-thinking.css +1584 -0
  109. painapple_code/static/css/24-background-tasks.css +359 -0
  110. painapple_code/static/css/25-context-block.css +252 -0
  111. painapple_code/static/css/26-session-setup.css +144 -0
  112. painapple_code/static/css/30-markdown.css +854 -0
  113. painapple_code/static/css/40-input.css +2418 -0
  114. painapple_code/static/css/41-autocomplete.css +127 -0
  115. painapple_code/static/css/42-activity-strip.css +97 -0
  116. painapple_code/static/css/43-file-autocomplete.css +121 -0
  117. painapple_code/static/css/44-snippets-autocomplete.css +224 -0
  118. painapple_code/static/css/45-selection-bar.css +433 -0
  119. painapple_code/static/css/46-keyboard-bar.css +119 -0
  120. painapple_code/static/css/47-keyboard-longpress.css +116 -0
  121. painapple_code/static/css/47-token-profile.css +106 -0
  122. painapple_code/static/css/50-welcome.css +3095 -0
  123. painapple_code/static/css/51-modals.css +301 -0
  124. painapple_code/static/css/52-utilities.css +77 -0
  125. painapple_code/static/css/53-image-upload.css +456 -0
  126. painapple_code/static/css/54-todo-list.css +102 -0
  127. painapple_code/static/css/55-file-upload.css +113 -0
  128. painapple_code/static/css/56-chat-navigator.css +218 -0
  129. painapple_code/static/css/56-permission-card.css +332 -0
  130. painapple_code/static/css/56-plan-approval.css +204 -0
  131. painapple_code/static/css/56-question-form.css +602 -0
  132. painapple_code/static/css/57-swipe-indicator.css +95 -0
  133. painapple_code/static/css/58-session-families.css +627 -0
  134. painapple_code/static/css/59-about.css +196 -0
  135. painapple_code/static/css/61-log-explorer.css +723 -0
  136. painapple_code/static/css/62-json-tree.css +489 -0
  137. painapple_code/static/css/62-skills-widget.css +650 -0
  138. painapple_code/static/css/63-commands-widget.css +30 -0
  139. painapple_code/static/css/63-plugins-widget.css +174 -0
  140. painapple_code/static/css/63-terminal.css +516 -0
  141. painapple_code/static/css/64-search-files-widget.css +448 -0
  142. painapple_code/static/css/65-snippets-widget.css +426 -0
  143. painapple_code/static/css/66-active-sessions.css +979 -0
  144. painapple_code/static/css/66-agents-widget.css +265 -0
  145. painapple_code/static/css/66-file-explorer-widget.css +855 -0
  146. painapple_code/static/css/66-git-panel.css +791 -0
  147. painapple_code/static/css/66-history-explorer.css +1496 -0
  148. painapple_code/static/css/67-diff-viewer.css +602 -0
  149. painapple_code/static/css/67-editor-view.css +748 -0
  150. painapple_code/static/css/67-file-preview-widget.css +1873 -0
  151. painapple_code/static/css/68-browser-widget.css +337 -0
  152. painapple_code/static/css/68-compare-wizard.css +355 -0
  153. painapple_code/static/css/68-csv-preview.css +106 -0
  154. painapple_code/static/css/68-diff-preview.css +103 -0
  155. painapple_code/static/css/68-helpers-install-widget.css +491 -0
  156. painapple_code/static/css/68-image-annotate.css +346 -0
  157. painapple_code/static/css/68-terminal-view.css +98 -0
  158. painapple_code/static/css/68-uploads-widget.css +229 -0
  159. painapple_code/static/css/69-cost-analytics.css +537 -0
  160. painapple_code/static/css/70-config-panel.css +3736 -0
  161. painapple_code/static/css/70-excalidraw.css +136 -0
  162. painapple_code/static/css/70-prompt-explorer.css +890 -0
  163. painapple_code/static/css/71-chart.css +111 -0
  164. painapple_code/static/css/72-discussion-sidebar.css +678 -0
  165. painapple_code/static/css/74-debug-logs.css +656 -0
  166. painapple_code/static/css/80-open-dialog.css +146 -0
  167. painapple_code/static/css/80-quick-switcher.css +383 -0
  168. painapple_code/static/css/80-widget-system.css +1763 -0
  169. painapple_code/static/css/81-quick-actions.css +1020 -0
  170. painapple_code/static/css/82-context-popover.css +1233 -0
  171. painapple_code/static/css/83-context-menu.css +339 -0
  172. painapple_code/static/css/85-lazy-loading.css +102 -0
  173. painapple_code/static/css/86-zen-mode.css +708 -0
  174. painapple_code/static/css/87-grid-switcher.css +346 -0
  175. painapple_code/static/css/login.css +198 -0
  176. painapple_code/static/dist/app.bundle.js +7177 -0
  177. painapple_code/static/feature-triage.html +446 -0
  178. painapple_code/static/icons/apple-touch-icon.png +0 -0
  179. painapple_code/static/icons/favicon-32.png +0 -0
  180. painapple_code/static/icons/favicon.ico +0 -0
  181. painapple_code/static/icons/icon-128.png +0 -0
  182. painapple_code/static/icons/icon-144.png +0 -0
  183. painapple_code/static/icons/icon-152.png +0 -0
  184. painapple_code/static/icons/icon-180.png +0 -0
  185. painapple_code/static/icons/icon-192.png +0 -0
  186. painapple_code/static/icons/icon-384.png +0 -0
  187. painapple_code/static/icons/icon-512.png +0 -0
  188. painapple_code/static/icons/icon-72.png +0 -0
  189. painapple_code/static/icons/icon-96.png +0 -0
  190. painapple_code/static/js/action-delegate.js +187 -0
  191. painapple_code/static/js/activity-strip.js +193 -0
  192. painapple_code/static/js/app/context-menus.js +619 -0
  193. painapple_code/static/js/app/file-preview.js +163 -0
  194. painapple_code/static/js/app/init-ui.js +574 -0
  195. painapple_code/static/js/app/input-events.js +561 -0
  196. painapple_code/static/js/app/input-resize.js +313 -0
  197. painapple_code/static/js/app/message-actions.js +419 -0
  198. painapple_code/static/js/app/panels.js +210 -0
  199. painapple_code/static/js/app/rendering-delegators.js +246 -0
  200. painapple_code/static/js/app/server-loading.js +365 -0
  201. painapple_code/static/js/app/session-ops.js +287 -0
  202. painapple_code/static/js/app/session-switch.js +443 -0
  203. painapple_code/static/js/app/settings-helpers.js +280 -0
  204. painapple_code/static/js/app/startup-welcome.js +290 -0
  205. painapple_code/static/js/app-context.js +157 -0
  206. painapple_code/static/js/app.js +2134 -0
  207. painapple_code/static/js/auth-fetch.js +46 -0
  208. painapple_code/static/js/background-tasks.js +274 -0
  209. painapple_code/static/js/boot.js +73 -0
  210. painapple_code/static/js/caret-position.js +171 -0
  211. painapple_code/static/js/chat-navigator.js +710 -0
  212. painapple_code/static/js/chat-search.js +323 -0
  213. painapple_code/static/js/command-executor.js +176 -0
  214. painapple_code/static/js/command-store.js +497 -0
  215. painapple_code/static/js/components.js +1414 -0
  216. painapple_code/static/js/config.js +218 -0
  217. painapple_code/static/js/context-menu.js +740 -0
  218. painapple_code/static/js/controllers/chat-controller.js +4252 -0
  219. painapple_code/static/js/controllers/dialog-controller.js +217 -0
  220. painapple_code/static/js/controllers/tab-controller.js +1828 -0
  221. painapple_code/static/js/controllers/thinking-controller.js +915 -0
  222. painapple_code/static/js/diff-utils.js +486 -0
  223. painapple_code/static/js/drafts-pill.js +95 -0
  224. painapple_code/static/js/editor-view.js +815 -0
  225. painapple_code/static/js/effort-settings.js +430 -0
  226. painapple_code/static/js/feature-triage.js +811 -0
  227. painapple_code/static/js/file-autocomplete.js +516 -0
  228. painapple_code/static/js/file-tabs.js +123 -0
  229. painapple_code/static/js/gestures.js +448 -0
  230. painapple_code/static/js/grid-switcher.js +334 -0
  231. painapple_code/static/js/input-handler.js +1472 -0
  232. painapple_code/static/js/keyboard-bar.js +495 -0
  233. painapple_code/static/js/layout-switcher.js +109 -0
  234. painapple_code/static/js/linkify-utils.js +200 -0
  235. painapple_code/static/js/login.js +141 -0
  236. painapple_code/static/js/open-dialog.js +879 -0
  237. painapple_code/static/js/orphan-terminals.js +373 -0
  238. painapple_code/static/js/path-utils.js +179 -0
  239. painapple_code/static/js/perf-marks.js +102 -0
  240. painapple_code/static/js/permission-settings.js +475 -0
  241. painapple_code/static/js/pointer-input.js +59 -0
  242. painapple_code/static/js/preview/json-tree-menu.js +226 -0
  243. painapple_code/static/js/preview/json-tree-toolbar.js +291 -0
  244. painapple_code/static/js/preview/json-tree.js +457 -0
  245. painapple_code/static/js/preview/preview-breadcrumb.js +387 -0
  246. painapple_code/static/js/preview/preview-edit.js +367 -0
  247. painapple_code/static/js/preview/preview-events.js +361 -0
  248. painapple_code/static/js/preview/preview-history.js +567 -0
  249. painapple_code/static/js/preview/preview-inline-edit.js +869 -0
  250. painapple_code/static/js/preview/preview-poll.js +108 -0
  251. painapple_code/static/js/preview/preview-render.js +572 -0
  252. painapple_code/static/js/preview/preview-search.js +421 -0
  253. painapple_code/static/js/preview/preview-state.js +256 -0
  254. painapple_code/static/js/preview/preview-utils.js +251 -0
  255. painapple_code/static/js/preview-plugins/chart-plugin.js +22 -0
  256. painapple_code/static/js/preview-plugins/csv-plugin.js +188 -0
  257. painapple_code/static/js/preview-plugins/diff-plugin.js +258 -0
  258. painapple_code/static/js/preview-plugins/excalidraw-plugin.js +25 -0
  259. painapple_code/static/js/preview-plugins/html-plugin.js +104 -0
  260. painapple_code/static/js/preview-plugins/image-plugin.js +20 -0
  261. painapple_code/static/js/preview-plugins/index.js +34 -0
  262. painapple_code/static/js/preview-plugins/json-plugin.js +99 -0
  263. painapple_code/static/js/preview-plugins/jsonl-plugin.js +152 -0
  264. painapple_code/static/js/preview-plugins/markdown-plugin.js +266 -0
  265. painapple_code/static/js/preview-plugins/panzoom-plugin.js +63 -0
  266. painapple_code/static/js/preview-plugins/plugin-helpers.js +282 -0
  267. painapple_code/static/js/project-color-picker.js +110 -0
  268. painapple_code/static/js/project-colors.js +163 -0
  269. painapple_code/static/js/prompt-favorites.js +187 -0
  270. painapple_code/static/js/quick-actions-menu.js +1736 -0
  271. painapple_code/static/js/quick-actions-registry.js +1239 -0
  272. painapple_code/static/js/quick-switcher/controller.js +294 -0
  273. painapple_code/static/js/quick-switcher/fuzzy-scorer.js +123 -0
  274. painapple_code/static/js/quick-switcher/index.js +30 -0
  275. painapple_code/static/js/quick-switcher/providers/base-provider.js +51 -0
  276. painapple_code/static/js/quick-switcher/providers/command-provider.js +56 -0
  277. painapple_code/static/js/quick-switcher/providers/file-provider.js +291 -0
  278. painapple_code/static/js/quick-switcher/providers/panel-provider.js +132 -0
  279. painapple_code/static/js/quick-switcher/providers/project-provider.js +361 -0
  280. painapple_code/static/js/quick-switcher/providers/read-files-provider.js +104 -0
  281. painapple_code/static/js/quick-switcher/providers/skills-provider.js +129 -0
  282. painapple_code/static/js/quick-switcher/registry.js +69 -0
  283. painapple_code/static/js/quick-switcher/ui/item.js +54 -0
  284. painapple_code/static/js/quick-switcher/ui/picker.js +464 -0
  285. painapple_code/static/js/recent-opens.js +40 -0
  286. painapple_code/static/js/scroll-manager.js +530 -0
  287. painapple_code/static/js/scroll-state-machine.js +359 -0
  288. painapple_code/static/js/selection/action-bar.js +793 -0
  289. painapple_code/static/js/selection/index.js +15 -0
  290. painapple_code/static/js/selection/selection-handler.js +877 -0
  291. painapple_code/static/js/selection/state.js +181 -0
  292. painapple_code/static/js/session/agent-progress.js +202 -0
  293. painapple_code/static/js/session/handle-agent-message.js +481 -0
  294. painapple_code/static/js/session/handle-message.js +550 -0
  295. painapple_code/static/js/session/interactive.js +587 -0
  296. painapple_code/static/js/session/message-store.js +271 -0
  297. painapple_code/static/js/session/messages.js +253 -0
  298. painapple_code/static/js/session/persistence.js +179 -0
  299. painapple_code/static/js/session/restore.js +136 -0
  300. painapple_code/static/js/session/sync.js +333 -0
  301. painapple_code/static/js/session-container-pool.js +371 -0
  302. painapple_code/static/js/session-setup-panel.js +274 -0
  303. painapple_code/static/js/session.js +1047 -0
  304. painapple_code/static/js/shortcut-hints.js +272 -0
  305. painapple_code/static/js/shortcuts.js +1146 -0
  306. painapple_code/static/js/skills-autocomplete.js +262 -0
  307. painapple_code/static/js/snippets-autocomplete.js +738 -0
  308. painapple_code/static/js/stash-refs-view.js +83 -0
  309. painapple_code/static/js/stash-ui.js +768 -0
  310. painapple_code/static/js/stash.js +785 -0
  311. painapple_code/static/js/status-bar.js +1184 -0
  312. painapple_code/static/js/token-profile.js +274 -0
  313. painapple_code/static/js/tool-renderer-blocks.js +1626 -0
  314. painapple_code/static/js/tool-renderer-thinking.js +813 -0
  315. painapple_code/static/js/tool-renderer.js +1500 -0
  316. painapple_code/static/js/tooltips.js +197 -0
  317. painapple_code/static/js/upload-manager.js +1019 -0
  318. painapple_code/static/js/utils.js +521 -0
  319. painapple_code/static/js/welcome/api.js +363 -0
  320. painapple_code/static/js/welcome/cards.js +626 -0
  321. painapple_code/static/js/welcome/context-menu.js +585 -0
  322. painapple_code/static/js/welcome/families.js +801 -0
  323. painapple_code/static/js/welcome/preview.js +234 -0
  324. painapple_code/static/js/welcome/state.js +129 -0
  325. painapple_code/static/js/welcome.js +1874 -0
  326. painapple_code/static/js/widget-system/base-widget.js +717 -0
  327. painapple_code/static/js/widget-system/device-manager.js +178 -0
  328. painapple_code/static/js/widget-system/event-bus.js +152 -0
  329. painapple_code/static/js/widget-system/icons.js +120 -0
  330. painapple_code/static/js/widget-system/index.js +49 -0
  331. painapple_code/static/js/widget-system/init.js +54 -0
  332. painapple_code/static/js/widget-system/types/bottom-sheet.js +307 -0
  333. painapple_code/static/js/widget-system/types/floating.js +915 -0
  334. painapple_code/static/js/widget-system/types/index.js +40 -0
  335. painapple_code/static/js/widget-system/types/modal.js +147 -0
  336. painapple_code/static/js/widget-system/types/sidebar.js +158 -0
  337. painapple_code/static/js/widget-system/types/tab.js +146 -0
  338. painapple_code/static/js/widget-system/types/top-sheet.js +203 -0
  339. painapple_code/static/js/widget-system/widget-manager.js +1187 -0
  340. painapple_code/static/js/widgets/about-widget.js +260 -0
  341. painapple_code/static/js/widgets/active-sessions-widget.js +760 -0
  342. painapple_code/static/js/widgets/agents-widget.js +897 -0
  343. painapple_code/static/js/widgets/browser-widget.js +579 -0
  344. painapple_code/static/js/widgets/commands-widget.js +694 -0
  345. painapple_code/static/js/widgets/config/commit-sections.js +449 -0
  346. painapple_code/static/js/widgets/config/dir-autocomplete.js +224 -0
  347. painapple_code/static/js/widgets/config/gestures.js +161 -0
  348. painapple_code/static/js/widgets/config/models-tab.js +988 -0
  349. painapple_code/static/js/widgets/config/quick-actions-tab.js +882 -0
  350. painapple_code/static/js/widgets/config/shortcut-editor.js +587 -0
  351. painapple_code/static/js/widgets/config/state.js +517 -0
  352. painapple_code/static/js/widgets/config/system-controls.js +90 -0
  353. painapple_code/static/js/widgets/config-widget.js +1535 -0
  354. painapple_code/static/js/widgets/cost-analytics-widget.js +621 -0
  355. painapple_code/static/js/widgets/debug-widget.js +693 -0
  356. painapple_code/static/js/widgets/diff-viewer-widget.js +1420 -0
  357. painapple_code/static/js/widgets/discussion-widget.js +1107 -0
  358. painapple_code/static/js/widgets/file-explorer-widget.js +2166 -0
  359. painapple_code/static/js/widgets/file-preview-widget.js +861 -0
  360. painapple_code/static/js/widgets/git-widget.js +794 -0
  361. painapple_code/static/js/widgets/helpers-install-widget.js +720 -0
  362. painapple_code/static/js/widgets/history-explorer-widget.js +1804 -0
  363. painapple_code/static/js/widgets/image-annotate-widget.js +951 -0
  364. painapple_code/static/js/widgets/image-preview-widget.js +692 -0
  365. painapple_code/static/js/widgets/index.js +176 -0
  366. painapple_code/static/js/widgets/log-explorer-widget.js +1057 -0
  367. painapple_code/static/js/widgets/plugins-widget.js +305 -0
  368. painapple_code/static/js/widgets/prompt-explorer-widget.js +1119 -0
  369. painapple_code/static/js/widgets/search-files-widget.js +529 -0
  370. painapple_code/static/js/widgets/skills-widget.js +843 -0
  371. painapple_code/static/js/widgets/snippets-widget.js +479 -0
  372. painapple_code/static/js/widgets/sub-agents-widget.js +333 -0
  373. painapple_code/static/js/widgets/tasks-widget.js +379 -0
  374. painapple_code/static/js/widgets/terminal/connection.js +283 -0
  375. painapple_code/static/js/widgets/terminal/gestures.js +357 -0
  376. painapple_code/static/js/widgets/terminal/init.js +642 -0
  377. painapple_code/static/js/widgets/terminal/link-providers.js +532 -0
  378. painapple_code/static/js/widgets/terminal/osc52.js +213 -0
  379. painapple_code/static/js/widgets/terminal/render.js +159 -0
  380. painapple_code/static/js/widgets/terminal/size.js +54 -0
  381. painapple_code/static/js/widgets/terminal/state.js +211 -0
  382. painapple_code/static/js/widgets/terminal/wrap-utils.js +101 -0
  383. painapple_code/static/js/widgets/terminal-widget.js +793 -0
  384. painapple_code/static/js/widgets/uploads-widget.js +461 -0
  385. painapple_code/static/js/widgets/zen-widget.js +1001 -0
  386. painapple_code/static/login.html +51 -0
  387. painapple_code/static/manifest.json +73 -0
  388. painapple_code/static/sw.js +293 -0
  389. painapple_code/static/vendor/LICENSES.txt +769 -0
  390. painapple_code/static/vendor/README.md +103 -0
  391. painapple_code/static/vendor/codemirror.js +35 -0
  392. painapple_code/static/vendor/codemirror.js.LEGAL.txt +304 -0
  393. painapple_code/static/vendor/github-markdown-dark.min.css +1124 -0
  394. painapple_code/static/vendor/highlight-github-dark.min.css +10 -0
  395. painapple_code/static/vendor/highlight-lang-dockerfile.min.js +8 -0
  396. painapple_code/static/vendor/highlight-lang-nginx.min.js +21 -0
  397. painapple_code/static/vendor/highlight-lang-properties.min.js +10 -0
  398. painapple_code/static/vendor/highlight-lang-scala.min.js +28 -0
  399. painapple_code/static/vendor/highlight.min.js +1213 -0
  400. painapple_code/static/vendor/marked.min.js +6 -0
  401. painapple_code/static/vendor/purify.min.js +3 -0
  402. painapple_code/static/vendor/xterm-addon-fit.js +2 -0
  403. painapple_code/static/vendor/xterm.css +209 -0
  404. painapple_code/static/vendor/xterm.js +2 -0
  405. painapple_code/static/web-client.html +635 -0
  406. painapple_code/subprocess_registry.py +535 -0
  407. painapple_code/tls_cert.py +79 -0
  408. painapple_code/tools/agents/shadow-git-helper.md +400 -0
  409. painapple_code/tools/excalidraw-to-svg.js +143 -0
  410. painapple_code/tools/install-helpers.sh +148 -0
  411. painapple_code/tools/shadow-git +320 -0
  412. painapple_code/tools/shadow-query +120 -0
  413. painapple_code/tools/vegalite-to-svg.js +79 -0
  414. painapple_code/turn_query.py +766 -0
  415. painapple_code/turn_tracker.py +221 -0
  416. painapple_code/utils/__init__.py +0 -0
  417. painapple_code/utils/agent_cli.py +306 -0
  418. painapple_code/utils/chart.py +103 -0
  419. painapple_code/utils/excalidraw.py +107 -0
  420. painapple_code/utils/file_lock.py +162 -0
  421. painapple_code/utils/file_paths.py +648 -0
  422. painapple_code/utils/generate_icons.py +251 -0
  423. painapple_code/utils/proc.py +205 -0
  424. painapple_code/utils/pty_backend.py +59 -0
  425. painapple_code/utils/pty_posix.py +160 -0
  426. painapple_code/utils/pty_windows.py +245 -0
  427. painapple_code/utils/token_profiles.py +91 -0
  428. painapple_code/viewer_templates.py +657 -0
  429. painapple_code/welcome_search.py +879 -0
  430. painapple_code-1.0.0.dist-info/METADATA +292 -0
  431. painapple_code-1.0.0.dist-info/RECORD +436 -0
  432. painapple_code-1.0.0.dist-info/WHEEL +5 -0
  433. painapple_code-1.0.0.dist-info/entry_points.txt +2 -0
  434. painapple_code-1.0.0.dist-info/licenses/LICENSE +661 -0
  435. painapple_code-1.0.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +261 -0
  436. painapple_code-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Right-click / long-press context menu for JSON tree nodes.
3
+ *
4
+ * The tree is rendered as an HTML string, so the menu can't hold a reference to
5
+ * the value it was opened on — it resolves the clicked element's
6
+ * `data-json-path` back through the parsed document (resolveJsonPath). That
7
+ * keeps the renderer pure and means the same code serves the JSON preview, the
8
+ * JSONL preview (one document per row) and any future caller.
9
+ */
10
+
11
+ import { copyToClipboard, showToast } from '../context-menu.js';
12
+ import { resolveJsonPath, formatJsonPath } from './json-tree.js';
13
+ import S from '../strings.js';
14
+
15
+ const LONG_PRESS_MS = 500;
16
+ const LONG_PRESS_SLOP = 10;
17
+
18
+ /**
19
+ * @param {Element} target - the tree root to delegate from ('.json-tree' / '.jsonl-rows')
20
+ * @param {object} opts
21
+ * @param {(path: string) => *} opts.getRoot - parsed document a path is rooted in
22
+ * @param {(text: string) => void} [opts.onSearch] - fill the search box with this text
23
+ * @param {(path: string, expanded: boolean) => void} [opts.onSetExpanded] - record one node's state
24
+ * @param {() => void} [opts.onAfterExpand] - called once after a bulk expand/collapse
25
+ */
26
+ export function bindJsonTreeContextMenu(target, opts = {}) {
27
+ const open = (x, y, el) => {
28
+ const node = el?.closest?.('[data-json-path]');
29
+ if (!node || !target.contains(node)) return false;
30
+ const items = buildItems(node, target, opts);
31
+ if (!items.length) return false;
32
+ const menu = window.app?.contextMenu;
33
+ if (!menu) return false;
34
+ menu.show(x, y, items);
35
+ return true;
36
+ };
37
+
38
+ target.addEventListener('contextmenu', (e) => {
39
+ // Let the browser's own menu handle a real text selection — that's the
40
+ // one case where the native "Copy" is more useful than ours.
41
+ if (!window.getSelection?.().isCollapsed) return;
42
+ if (open(e.clientX, e.clientY, e.target)) {
43
+ e.preventDefault();
44
+ e.stopPropagation();
45
+ }
46
+ });
47
+
48
+ // Touch long-press. iOS synthesizes a click after touchend, which would
49
+ // toggle the node the menu just opened on, so it gets swallowed.
50
+ let timer = null;
51
+ let startX = 0;
52
+ let startY = 0;
53
+ let fired = false;
54
+
55
+ const cancel = () => {
56
+ clearTimeout(timer);
57
+ timer = null;
58
+ };
59
+
60
+ target.addEventListener('touchstart', (e) => {
61
+ if (e.touches.length !== 1) return cancel();
62
+ const t = e.touches[0];
63
+ startX = t.clientX;
64
+ startY = t.clientY;
65
+ fired = false;
66
+ const el = e.target;
67
+ timer = setTimeout(() => {
68
+ timer = null;
69
+ fired = open(startX, startY, el);
70
+ }, LONG_PRESS_MS);
71
+ }, { passive: true });
72
+
73
+ target.addEventListener('touchmove', (e) => {
74
+ const t = e.touches[0];
75
+ if (!t) return;
76
+ if (Math.abs(t.clientX - startX) > LONG_PRESS_SLOP
77
+ || Math.abs(t.clientY - startY) > LONG_PRESS_SLOP) cancel();
78
+ }, { passive: true });
79
+
80
+ target.addEventListener('touchend', cancel, { passive: true });
81
+ target.addEventListener('touchcancel', cancel, { passive: true });
82
+
83
+ target.addEventListener('click', (e) => {
84
+ if (!fired) return;
85
+ fired = false;
86
+ e.preventDefault();
87
+ e.stopPropagation();
88
+ }, { capture: true });
89
+ }
90
+
91
+ // ─── Menu contents ───────────────────────────────────────────────────────
92
+
93
+ function buildItems(node, target, opts) {
94
+ const M = S.preview?.json_menu || {};
95
+ const path = node.dataset.jsonPath;
96
+ const root = opts.getRoot?.(path);
97
+ const resolved = root === undefined ? { found: false } : resolveJsonPath(root, path);
98
+ const items = [];
99
+
100
+ if (resolved.found) {
101
+ const { value, key } = resolved;
102
+ const pretty = safeStringify(value);
103
+ const isScalar = value === null || typeof value !== 'object';
104
+ // For a string, "value" means the string itself — copying `"foo"` with
105
+ // the quotes baked in is almost never what you wanted.
106
+ const plain = typeof value === 'string' ? value
107
+ : isScalar ? String(value)
108
+ : pretty;
109
+
110
+ items.push({
111
+ label: M.copy_value || 'Copy value',
112
+ icon: 'copy',
113
+ action: () => copy(plain, M.copied_value || 'Value copied'),
114
+ });
115
+ if (pretty !== null && pretty !== plain) {
116
+ items.push({
117
+ label: M.copy_json || 'Copy as JSON',
118
+ icon: 'code',
119
+ action: () => copy(pretty, M.copied_json || 'JSON copied'),
120
+ });
121
+ }
122
+ if (key) {
123
+ items.push({
124
+ label: M.copy_key || 'Copy key',
125
+ action: () => copy(key, M.copied_key || 'Key copied'),
126
+ });
127
+ if (isScalar) {
128
+ items.push({
129
+ label: M.copy_pair || 'Copy "key": value',
130
+ action: () => copy(`${JSON.stringify(key)}: ${safeStringify(value, 0)}`,
131
+ M.copied_pair || 'Entry copied'),
132
+ });
133
+ }
134
+ }
135
+ items.push({
136
+ label: M.copy_path || 'Copy path',
137
+ action: () => copy(formatJsonPath(path), M.copied_path || 'Path copied'),
138
+ });
139
+
140
+ if (isScalar && value !== null && opts.onSearch) {
141
+ const term = String(value);
142
+ if (term && term.length <= 200) {
143
+ items.push({ type: 'separator' });
144
+ items.push({
145
+ label: M.search_value || 'Search for this value',
146
+ icon: 'search',
147
+ action: () => opts.onSearch(term),
148
+ });
149
+ }
150
+ }
151
+ }
152
+
153
+ // Subtree expand/collapse — only meaningful where there's something to fold.
154
+ const subtree = node.classList.contains('json-collapsible')
155
+ ? node
156
+ : node.querySelector('.json-collapsible');
157
+ if (subtree) {
158
+ items.push({ type: 'separator' });
159
+ items.push({
160
+ label: M.expand_subtree || 'Expand subtree',
161
+ icon: 'chevronDown',
162
+ action: () => setSubtree(node, true, opts),
163
+ });
164
+ items.push({
165
+ label: M.collapse_subtree || 'Collapse subtree',
166
+ icon: 'chevronRight',
167
+ action: () => setSubtree(node, false, opts),
168
+ });
169
+ }
170
+
171
+ if (root !== undefined) {
172
+ const whole = safeStringify(root);
173
+ if (whole !== null) {
174
+ items.push({ type: 'separator' });
175
+ items.push({
176
+ label: opts.rootLabel || M.copy_document || 'Copy whole document',
177
+ icon: 'file',
178
+ action: () => copy(whole, opts.rootToast || M.copied_document || 'Document copied'),
179
+ });
180
+ }
181
+ }
182
+
183
+ return items;
184
+ }
185
+
186
+ /**
187
+ * Fold or unfold everything under `node` in one shot: the DOM classes AND the
188
+ * override records, so the next re-render (any keystroke in the search box)
189
+ * doesn't undo it.
190
+ */
191
+ function setSubtree(node, expanded, opts) {
192
+ const nodes = [];
193
+ if (node.classList.contains('json-collapsible')) nodes.push(node);
194
+ nodes.push(...node.querySelectorAll('.json-collapsible'));
195
+ for (const el of nodes) {
196
+ el.classList.toggle('expanded', expanded);
197
+ opts.onSetExpanded?.(el.dataset.jsonPath, expanded);
198
+ }
199
+ for (const el of node.querySelectorAll('.json-nested-container')) {
200
+ const nested = el.querySelector('.json-nested-content');
201
+ const preview = el.querySelector('.json-string-preview');
202
+ const btn = el.querySelector('.json-parse-btn');
203
+ if (!nested) continue;
204
+ nested.hidden = !expanded;
205
+ if (preview) preview.hidden = expanded;
206
+ btn?.classList.toggle('active', expanded);
207
+ opts.onSetExpanded?.(el.dataset.jsonPath, expanded);
208
+ }
209
+ opts.onAfterExpand?.();
210
+ }
211
+
212
+ /** JSON.stringify that survives circular refs / BigInt rather than throwing mid-menu. */
213
+ function safeStringify(value, indent = 2) {
214
+ try {
215
+ const out = JSON.stringify(value, null, indent);
216
+ return out === undefined ? null : out;
217
+ } catch (e) {
218
+ return null;
219
+ }
220
+ }
221
+
222
+ function copy(text, toast) {
223
+ copyToClipboard(text).then(ok => {
224
+ showToast(ok ? toast : (S.preview?.json_menu?.copy_failed || 'Copy failed'));
225
+ });
226
+ }
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Shared search / expand toolbar for the JSON and JSONL tree previews.
3
+ *
4
+ * Both plugins render the same control row and need the same behavior, so it
5
+ * lives here instead of being copy-pasted twice with two sets of bugs.
6
+ *
7
+ * What it owns:
8
+ * - the query, debounced into a re-render (the tree is a render-to-string
9
+ * module, so search *is* a re-render — see json-tree.js)
10
+ * - match count + prev/next navigation. The tree search used to highlight and
11
+ * nothing else: no count, no way to jump, Enter did nothing, and Ctrl+F
12
+ * handed you an input with none of the affordances the generic preview
13
+ * search bar has.
14
+ * - caret/focus survival across those re-renders
15
+ * - expand-all / collapse-all and per-node toggles as *state*, so the next
16
+ * keystroke doesn't throw them away
17
+ *
18
+ * The count is read back out of the DOM rather than tallied during render:
19
+ * whatever is highlighted and visible on screen is by definition what "N of M"
20
+ * has to mean, and there's no second bookkeeping path to drift out of sync.
21
+ */
22
+
23
+ import { escapeHtml } from '../utils.js';
24
+ import { collectVisibleMatches } from './json-tree.js';
25
+ import { fns } from './preview-state.js';
26
+ import S from '../strings.js';
27
+
28
+ const DEBOUNCE_MS = 120;
29
+
30
+ export function initJsonToolbarState() {
31
+ return {
32
+ search: '',
33
+ // path → explicit expand state from a user toggle, so a re-render (i.e.
34
+ // every keystroke in the search box) doesn't undo it. Only expand-all /
35
+ // collapse-all resets the set.
36
+ overrides: new Map(),
37
+ // null | 'expand' | 'collapse' — sticky expand-all / collapse-all
38
+ mode: null,
39
+ matchIndex: 0,
40
+ };
41
+ }
42
+
43
+ /** Tolerate a pluginState that was reset to a bare {} elsewhere. */
44
+ export function normalizeToolbarState(ps) {
45
+ if (!ps) return initJsonToolbarState();
46
+ if (typeof ps.search !== 'string') ps.search = '';
47
+ if (!(ps.overrides instanceof Map)) ps.overrides = new Map();
48
+ if (ps.mode !== 'expand' && ps.mode !== 'collapse') ps.mode = null;
49
+ if (typeof ps.matchIndex !== 'number' || ps.matchIndex < 0) ps.matchIndex = 0;
50
+ return ps;
51
+ }
52
+
53
+ /** Render options for the tree itself, derived from toolbar state. */
54
+ export function treeOptions(ps, extra = {}) {
55
+ return {
56
+ search: (ps.search || '').trim().toLowerCase(),
57
+ expandAll: ps.mode === 'expand',
58
+ collapseAll: ps.mode === 'collapse',
59
+ overrides: ps.overrides,
60
+ ...extra,
61
+ };
62
+ }
63
+
64
+ export function renderJsonToolbar(ps) {
65
+ const query = (ps.search || '').trim();
66
+ const hide = query ? '' : ' hidden';
67
+ const placeholder = S.preview?.json_search_placeholder || 'Search keys and values…';
68
+
69
+ return `
70
+ <div class="json-toolbar">
71
+ <div class="json-search-wrap">
72
+ <svg class="json-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
73
+ <circle cx="11" cy="11" r="8"/>
74
+ <line x1="21" y1="21" x2="16.65" y2="16.65"/>
75
+ </svg>
76
+ <input type="text" class="json-search-input" placeholder="${escapeHtml(placeholder)}" value="${escapeHtml(ps.search || '')}" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
77
+ <span class="json-search-count"${hide}></span>
78
+ <button class="json-search-nav" data-action="search-prev" data-tooltip="${escapeHtml(S.preview?.json_search_prev || 'Previous match (Shift+Enter)')}"${hide}>
79
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"/></svg>
80
+ </button>
81
+ <button class="json-search-nav" data-action="search-next" data-tooltip="${escapeHtml(S.preview?.json_search_next || 'Next match (Enter)')}"${hide}>
82
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
83
+ </button>
84
+ <button class="json-search-clear" data-action="clear-search" data-tooltip="${escapeHtml(S.preview?.json_search_clear || 'Clear (Escape)')}"${hide}>×</button>
85
+ </div>
86
+ <button class="json-ctrl-btn ${ps.mode === 'expand' ? 'active' : ''}" data-action="expand-all" data-tooltip="${escapeHtml(S.preview?.json_expand_all || 'Expand all')}">
87
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
88
+ </button>
89
+ <button class="json-ctrl-btn ${ps.mode === 'collapse' ? 'active' : ''}" data-action="collapse-all" data-tooltip="${escapeHtml(S.preview?.json_collapse_all || 'Collapse all')}">
90
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"/></svg>
91
+ </button>
92
+ </div>
93
+ `;
94
+ }
95
+
96
+ /**
97
+ * Wire the toolbar. Call once per render from the plugin's setupEvents.
98
+ *
99
+ * @param {Element} container - the preview widget container
100
+ * @param {object} ps - plugin state (see initJsonToolbarState)
101
+ * @param {object} helpers - plugin helpers ({ rerenderContent })
102
+ * @param {string} treeSelector - root the matches live under ('.json-tree', '.jsonl-rows')
103
+ */
104
+ export function bindJsonToolbar(container, ps, helpers, treeSelector) {
105
+ normalizeToolbarState(ps);
106
+
107
+ const refresh = (opts) => refreshMatches(container, ps, treeSelector, opts);
108
+ const rerender = (scroll) => {
109
+ const input = liveContainer(container).querySelector('.json-search-input');
110
+ const focused = input && document.activeElement === input;
111
+ const selStart = focused ? input.selectionStart : null;
112
+ const selEnd = focused ? input.selectionEnd : null;
113
+ helpers.rerenderContent();
114
+ if (focused) restoreSearchFocus(container, ps, selStart, selEnd);
115
+ refresh({ scroll });
116
+ };
117
+
118
+ const applyQuery = (value) => {
119
+ if (value === ps.search) return false;
120
+ ps.search = value;
121
+ // New query, new match set — start navigation at the top. Manual
122
+ // expand/collapse overrides are deliberately KEPT (they outlive a
123
+ // re-render); auto-reveal outranks them anyway, see resolveExpanded.
124
+ ps.matchIndex = 0;
125
+ rerender(true);
126
+ return true;
127
+ };
128
+
129
+ const searchInput = container.querySelector('.json-search-input');
130
+ let debounce = null;
131
+ const flush = () => {
132
+ clearTimeout(debounce);
133
+ debounce = null;
134
+ const input = liveContainer(container).querySelector('.json-search-input');
135
+ return input ? applyQuery(input.value) : false;
136
+ };
137
+
138
+ if (searchInput) {
139
+ searchInput.addEventListener('input', () => {
140
+ clearTimeout(debounce);
141
+ debounce = setTimeout(flush, DEBOUNCE_MS);
142
+ });
143
+
144
+ searchInput.addEventListener('keydown', (e) => {
145
+ if (e.key === 'Enter') {
146
+ e.preventDefault();
147
+ // Enter straight after typing must search what's in the box,
148
+ // not what the debounce has committed so far.
149
+ if (!flush()) navigate(container, ps, treeSelector, e.shiftKey ? -1 : 1);
150
+ } else if (e.key === 'Escape') {
151
+ // Don't let Escape bubble out and close the whole preview.
152
+ e.preventDefault();
153
+ e.stopPropagation();
154
+ clearTimeout(debounce);
155
+ if (searchInput.value) {
156
+ searchInput.value = '';
157
+ applyQuery('');
158
+ } else {
159
+ searchInput.blur();
160
+ }
161
+ }
162
+ });
163
+ }
164
+
165
+ container.querySelectorAll('.json-ctrl-btn, .json-search-nav, .json-search-clear').forEach(btn => {
166
+ btn.addEventListener('click', () => {
167
+ const act = btn.dataset.action;
168
+ if (act === 'expand-all' || act === 'collapse-all') {
169
+ // State, not a DOM sweep: the old version toggled classes
170
+ // directly, so the very next re-render (a keystroke, a poll)
171
+ // silently undid it.
172
+ const next = act === 'expand-all' ? 'expand' : 'collapse';
173
+ ps.mode = ps.mode === next ? null : next;
174
+ ps.overrides = new Map();
175
+ rerender(false);
176
+ } else if (act === 'clear-search') {
177
+ clearTimeout(debounce);
178
+ applyQuery('');
179
+ } else if (act === 'search-prev') {
180
+ navigate(container, ps, treeSelector, -1);
181
+ } else if (act === 'search-next') {
182
+ navigate(container, ps, treeSelector, 1);
183
+ }
184
+ });
185
+ });
186
+
187
+ const record = (path, expanded) => {
188
+ if (path) ps.overrides.set(path, expanded);
189
+ };
190
+
191
+ return {
192
+ onToggle(path, expanded) {
193
+ record(path, expanded);
194
+ // Folding/unfolding changes which matches are on screen.
195
+ refresh({ scroll: false });
196
+ },
197
+ // Batched variant for the context menu's expand/collapse-subtree, which
198
+ // touches many nodes and wants a single recount at the end.
199
+ record,
200
+ refresh,
201
+ /** Drive the search box from elsewhere (e.g. "Search for this value"). */
202
+ setQuery(value) {
203
+ clearTimeout(debounce);
204
+ const input = liveContainer(container).querySelector('.json-search-input');
205
+ if (input) input.value = value;
206
+ if (!applyQuery(value)) refresh({ scroll: true });
207
+ liveContainer(container).querySelector('.json-search-input')?.focus();
208
+ },
209
+ };
210
+ }
211
+
212
+ // ─── Internal helpers ────────────────────────────────────────────────────
213
+
214
+ /**
215
+ * A re-render can swap the container node out from under us (the floating
216
+ * widget path rebuilds it); fall back to the live instance lookup rather than
217
+ * writing into a detached tree.
218
+ */
219
+ function liveContainer(container) {
220
+ if (container?.isConnected) return container;
221
+ return fns.findPreviewContainer?.() || container;
222
+ }
223
+
224
+ function restoreSearchFocus(container, ps, selStart, selEnd) {
225
+ const apply = (guard) => {
226
+ const input = liveContainer(container).querySelector('.json-search-input');
227
+ if (!input) return;
228
+ // Never clobber a caret the user has moved on with since the re-render.
229
+ if (guard && input.value !== ps.search) return;
230
+ if (document.activeElement !== input) input.focus();
231
+ const end = input.value.length;
232
+ try {
233
+ input.setSelectionRange(Math.min(selStart ?? end, end), Math.min(selEnd ?? end, end));
234
+ } catch (e) { /* input type without selection support */ }
235
+ };
236
+ apply(false);
237
+ requestAnimationFrame(() => apply(true));
238
+ }
239
+
240
+ function refreshMatches(container, ps, treeSelector, { scroll = false } = {}) {
241
+ const root = liveContainer(container);
242
+ const treeRoot = root.querySelector(treeSelector);
243
+ const matches = collectVisibleMatches(treeRoot);
244
+ const query = (ps.search || '').trim();
245
+
246
+ ps.matchIndex = matches.length
247
+ ? Math.min(Math.max(ps.matchIndex | 0, 0), matches.length - 1)
248
+ : 0;
249
+
250
+ root.querySelectorAll('.json-match.current').forEach(el => el.classList.remove('current'));
251
+ const current = matches[ps.matchIndex];
252
+ if (current) current.classList.add('current');
253
+
254
+ const countEl = root.querySelector('.json-search-count');
255
+ if (countEl) {
256
+ countEl.hidden = !query;
257
+ countEl.textContent = matches.length
258
+ ? `${ps.matchIndex + 1} of ${matches.length}`
259
+ : (query ? (S.preview?.json_search_no_results || 'No results') : '');
260
+ countEl.classList.toggle('no-results', !!query && matches.length === 0);
261
+ }
262
+ root.querySelectorAll('.json-search-nav').forEach(btn => {
263
+ btn.hidden = !query;
264
+ btn.disabled = matches.length === 0;
265
+ });
266
+ const clearBtn = root.querySelector('.json-search-clear');
267
+ if (clearBtn) clearBtn.hidden = !query;
268
+
269
+ if (scroll && current) scrollMatchIntoView(root, current);
270
+ return matches;
271
+ }
272
+
273
+ function navigate(container, ps, treeSelector, direction) {
274
+ const matches = refreshMatches(container, ps, treeSelector);
275
+ if (!matches.length) return;
276
+ ps.matchIndex = (ps.matchIndex + direction + matches.length) % matches.length;
277
+ refreshMatches(container, ps, treeSelector, { scroll: true });
278
+ }
279
+
280
+ /**
281
+ * Manual scrollTop math rather than scrollIntoView: iOS WKWebView ignores
282
+ * `block:'center'` on overflow:auto containers, which is the same reason
283
+ * preview-search.js does it by hand.
284
+ */
285
+ function scrollMatchIntoView(container, el) {
286
+ const scroller = container.querySelector('.preview-body') || container;
287
+ const containerRect = scroller.getBoundingClientRect();
288
+ const matchRect = el.getBoundingClientRect();
289
+ const offset = matchRect.top - containerRect.top + scroller.scrollTop;
290
+ scroller.scrollTop = Math.max(0, offset - scroller.clientHeight / 2 + matchRect.height / 2);
291
+ }