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,475 @@
1
+ /**
2
+ * Permission Settings - Permission Level Configuration
3
+ *
4
+ * Manages the permission button near the input area that controls
5
+ * the agent's permission mode (bypassPermissions/YOLO or plan).
6
+ *
7
+ * Permission levels are stored PER SESSION, allowing different modes
8
+ * for different conversations.
9
+ *
10
+ * The mode list itself is engine vocabulary: each provider self-describes
11
+ * its modes (value/label/desc/color), delivered alongside the permission
12
+ * endpoints (`GET /api/session/{id}/permission-mode`,
13
+ * `GET /api/app/default-permissions`). The strings.yaml level table is
14
+ * only the pre-fetch fallback, so e.g. the claude-sdk provider's extra
15
+ * "Ask" mode appears without any frontend edit.
16
+ */
17
+
18
+ import { CONFIG } from './config.js';
19
+ import S from './strings.js';
20
+ // Engine registry lookup (status-bar owns the /api/providers cache; it does
21
+ // not import this module, so no cycle). Used to seed the mode vocabulary
22
+ // synchronously on tab switch — the fetches below only confirm.
23
+ import { engineInfo } from './status-bar.js';
24
+
25
+ // Pre-fetch fallback modes from strings.yaml (shape matches the provider's
26
+ // permission_modes() dicts). Replaced by server-delivered lists on load.
27
+ const FALLBACK_MODES = S.permissions.order.map(key => {
28
+ const lvl = S.permissions.levels[key];
29
+ return { value: key, label: lvl.popup_label, desc: lvl.description, color: lvl.color };
30
+ });
31
+
32
+ class PermissionSettingsManager {
33
+ constructor() {
34
+ this.btn = null;
35
+ this.popup = null;
36
+ this.labelEl = null;
37
+ this.isOpen = false;
38
+ this.currentLevel = 'dontAsk'; // mode value from this.modes
39
+ this.currentSessionId = null;
40
+ this.globalDefault = 'dontAsk'; // Default for normal chat
41
+ this.modes = FALLBACK_MODES; // active session's engine modes
42
+ this.defaultModes = FALLBACK_MODES; // effective default engine's modes
43
+ }
44
+
45
+ /**
46
+ * Initialize the permission settings UI
47
+ */
48
+ init() {
49
+ this.btn = document.getElementById('permission-btn');
50
+ this.popup = document.getElementById('permission-popup');
51
+ this.labelEl = this.btn?.querySelector('.permission-label');
52
+
53
+ if (!this.btn || !this.popup) {
54
+ console.warn('Permission settings UI elements not found');
55
+ return;
56
+ }
57
+
58
+ // Generate popup HTML from the current mode list
59
+ this._renderPopup();
60
+
61
+ // Toggle popup on button click
62
+ this.btn.addEventListener('click', (e) => {
63
+ e.stopPropagation();
64
+ this.toggle();
65
+ });
66
+
67
+ // Handle preset selection + "Set as default"
68
+ this.popup.addEventListener('click', (e) => {
69
+ const preset = e.target.closest('.permission-preset');
70
+ if (preset) {
71
+ this.selectLevel(preset.dataset.level);
72
+ return;
73
+ }
74
+ const def = e.target.closest('.permission-set-default');
75
+ if (def) this._saveAsGlobalDefault();
76
+ });
77
+
78
+ // Close on outside click
79
+ document.addEventListener('click', (e) => {
80
+ if (this.isOpen && !this.btn.contains(e.target) && !this.popup.contains(e.target)) {
81
+ this.close();
82
+ }
83
+ });
84
+
85
+ // Close on Escape
86
+ document.addEventListener('keydown', (e) => {
87
+ if (e.key === 'Escape' && this.isOpen) {
88
+ this.close();
89
+ e.stopPropagation();
90
+ }
91
+ });
92
+
93
+ // Load global defaults
94
+ this.loadGlobalDefaults();
95
+ }
96
+
97
+ /**
98
+ * Look up a mode by value in the active list (fallback: strings.yaml set,
99
+ * so a level persisted under another engine still labels the button).
100
+ */
101
+ _modeInfo(value) {
102
+ return this.modes.find(m => m.value === value)
103
+ || FALLBACK_MODES.find(m => m.value === value)
104
+ || null;
105
+ }
106
+
107
+ /**
108
+ * Adopt a server-delivered mode list (the session engine's own vocabulary)
109
+ * and re-render the popup if it changed.
110
+ */
111
+ _applyModes(modes) {
112
+ const next = (Array.isArray(modes) && modes.length) ? modes : this.defaultModes;
113
+ const same = next.length === this.modes.length
114
+ && next.every((m, i) => m.value === this.modes[i].value);
115
+ this.modes = next;
116
+ if (!same) {
117
+ this._renderPopup();
118
+ if (this.isOpen) this.updatePresetSelection();
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Generate popup HTML from the active mode list
124
+ */
125
+ _renderPopup() {
126
+ const presets = this.modes.map(m => {
127
+ return `<button class="permission-preset" data-level="${m.value}">
128
+ <span class="preset-dot" style="background:${m.color}"></span>
129
+ <div class="preset-info">
130
+ <span class="preset-label">${m.label}<span class="preset-default-tag">default</span></span>
131
+ <span class="preset-desc">${m.desc}</span>
132
+ </div>
133
+ <span class="preset-check"></span>
134
+ </button>`;
135
+ }).join('');
136
+
137
+ this.popup.innerHTML = `
138
+ <div class="permission-popup-header">${S.permissions.header}</div>
139
+ <div class="permission-presets">${presets}</div>
140
+ <div class="permission-popup-footer">
141
+ <button class="permission-set-default">Set as default</button>
142
+ </div>`;
143
+ }
144
+
145
+ /**
146
+ * Synchronous seed on session switch: adopt the session engine's mode
147
+ * vocabulary from the registry and the session's cached/pending level, so
148
+ * the first paint after a tab switch is engine-correct. The server fetch
149
+ * in setSession() then merely confirms (and refreshes the caches).
150
+ */
151
+ _seedFromSession(opts = {}) {
152
+ const session = window.app?.activeSession;
153
+ const engName = session?.provider
154
+ || (opts.pendingProvider !== undefined
155
+ ? opts.pendingProvider
156
+ : session?.pendingProvider)
157
+ || null;
158
+ const eng = engName ? engineInfo(engName) : null;
159
+ if (eng?.permission_modes?.length) {
160
+ this._applyModes(eng.permission_modes);
161
+ } else if (!engName) {
162
+ this._applyModes(this.defaultModes);
163
+ }
164
+ const cached = [session?.permissionLevel, session?.pendingPermission,
165
+ this.globalDefault].find(v => v && this._modeInfo(v));
166
+ this.currentLevel = cached
167
+ || eng?.default_permission_mode
168
+ || this.currentLevel;
169
+ this.updateButtonState();
170
+ }
171
+
172
+ /**
173
+ * Set current session and load its permission level
174
+ * Called when switching tabs/sessions
175
+ */
176
+ async setSession(sessionId, opts = {}) {
177
+ this.currentSessionId = sessionId;
178
+ this._seedFromSession(opts);
179
+
180
+ if (!sessionId) {
181
+ // Unconnected session — show the modes of the engine it WILL run
182
+ // on: the tab's picked engine (pendingProvider) if any, else the
183
+ // box default. Restore any pending choice stashed on the session
184
+ // object, or fall back to global default.
185
+ const session = window.app?.activeSession;
186
+ const pendingProvider = opts.pendingProvider !== undefined
187
+ ? opts.pendingProvider
188
+ : (session?.pendingProvider || null);
189
+ if (pendingProvider) {
190
+ try {
191
+ const r = await fetch(`${CONFIG.API_BASE}/api/app/default-permissions?provider=${encodeURIComponent(pendingProvider)}`);
192
+ // Bail if a newer setSession() superseded us during the await.
193
+ if (this.currentSessionId !== sessionId) return;
194
+ if (r.ok) {
195
+ const data = await r.json();
196
+ if (Array.isArray(data.modes) && data.modes.length) {
197
+ this._applyModes(data.modes);
198
+ }
199
+ // A stashed choice only survives if this engine speaks it.
200
+ this.currentLevel = this._modeInfo(session?.pendingPermission)
201
+ ? session.pendingPermission
202
+ : (this._modeInfo(data.default_level) ? data.default_level : this.currentLevel);
203
+ this.updateButtonState();
204
+ return;
205
+ }
206
+ } catch (err) { /* fall through to default-engine modes */ }
207
+ if (this.currentSessionId !== sessionId) return;
208
+ }
209
+ this._applyModes(this.defaultModes);
210
+ this.currentLevel = session?.pendingPermission || this.globalDefault;
211
+ this.updateButtonState();
212
+ return;
213
+ }
214
+
215
+ try {
216
+ const response = await fetch(`${CONFIG.API_BASE}/api/session/${sessionId}/permission-mode`);
217
+ // Bail if a newer setSession() superseded us during the await.
218
+ if (this.currentSessionId !== sessionId) return;
219
+ if (response.ok) {
220
+ const data = await response.json();
221
+ this._applyModes(data.modes);
222
+ this.globalDefault = this._modeInfo(data.global_default) ? data.global_default : 'dontAsk';
223
+ // A brand-new session has no stored per-session level yet. Fall
224
+ // back to the global default (e.g. YOLO) rather than a hardcoded
225
+ // 'dontAsk', which would silently override it after first send.
226
+ // A global default this engine doesn't speak (cross-provider)
227
+ // falls through to the engine's own default.
228
+ this.currentLevel = this._modeInfo(data.permission_level)
229
+ ? data.permission_level
230
+ : (this._modeInfo(this.globalDefault)
231
+ ? this.globalDefault
232
+ : (data.provider_default || 'dontAsk'));
233
+ // Cache on the session so the next switch seeds synchronously.
234
+ const sess = window.app?.sessionManager?.sessions
235
+ ?.find(s => s.storeId === sessionId);
236
+ if (sess) sess.permissionLevel = this.currentLevel;
237
+ this.updateButtonState();
238
+ }
239
+ } catch (err) {
240
+ console.error('Error loading session permission mode:', err);
241
+ this.currentLevel = this.globalDefault;
242
+ this.updateButtonState();
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Toggle popup visibility
248
+ */
249
+ toggle() {
250
+ if (this.isOpen) {
251
+ this.close();
252
+ } else {
253
+ this.open();
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Open popup
259
+ */
260
+ open() {
261
+ // Position popup above the button (fixed positioning)
262
+ const btnRect = this.btn.getBoundingClientRect();
263
+ this.popup.style.bottom = `${window.innerHeight - btnRect.top + 8}px`;
264
+ this.popup.style.right = `${window.innerWidth - btnRect.right}px`;
265
+
266
+ this.popup.classList.add('open');
267
+ this.isOpen = true;
268
+ this.updatePresetSelection();
269
+ }
270
+
271
+ /**
272
+ * Close popup
273
+ */
274
+ close() {
275
+ this.popup.classList.remove('open');
276
+ this.isOpen = false;
277
+ }
278
+
279
+ /**
280
+ * Select a permission level and apply to session
281
+ */
282
+ async selectLevel(level) {
283
+ if (level === this.currentLevel) {
284
+ this.close();
285
+ return;
286
+ }
287
+
288
+ const previousLevel = this.currentLevel;
289
+ this.currentLevel = level;
290
+ this.updateButtonState();
291
+ this.close();
292
+
293
+ if (!this.currentSessionId) {
294
+ // No storeId yet — stash on the active session so the choice
295
+ // survives tab switches. Picked up on first connect. Use
296
+ // "Set as default" to promote to global.
297
+ const session = window.app?.activeSession;
298
+ if (session) session.pendingPermission = level;
299
+ return;
300
+ }
301
+
302
+ // Cache on the session so the next tab switch seeds synchronously.
303
+ {
304
+ const sess = window.app?.activeSession;
305
+ if (sess?.storeId === this.currentSessionId) sess.permissionLevel = level;
306
+ }
307
+
308
+ // Save to session meta
309
+ try {
310
+ await fetch(`${CONFIG.API_BASE}/api/session/${this.currentSessionId}/permission-mode`, {
311
+ method: 'PUT',
312
+ headers: { 'Content-Type': 'application/json' },
313
+ body: JSON.stringify({ permission_level: level })
314
+ });
315
+ } catch (err) {
316
+ console.error('Error saving session permission mode:', err);
317
+ }
318
+
319
+ // If there's a WebSocket connection, send permission mode change.
320
+ // On live-controls providers (claude-sdk) the server applies it to
321
+ // the running engine immediately — even mid-turn; elsewhere it takes
322
+ // effect on the next message via the lazy respawn. The reply's
323
+ // `applied` field says which one happened.
324
+ const session = window.app?.activeSession;
325
+ if (session?.ws && session.ws.readyState === WebSocket.OPEN) {
326
+ session.ws.send(JSON.stringify({
327
+ type: 'set_permission_mode',
328
+ mode: level
329
+ }));
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Save current level as the global default (explicit user action from
335
+ * the popup footer). Mirrors token-profile's _saveAsGlobalDefault.
336
+ */
337
+ async _saveAsGlobalDefault() {
338
+ const level = this.currentLevel;
339
+ this.close();
340
+ await this.saveToGlobal(level);
341
+
342
+ // Sync Settings panel dropdown if open
343
+ const sel = document.querySelector('#default-permission-level');
344
+ if (sel) sel.value = level;
345
+ }
346
+
347
+ /**
348
+ * Save to global config (when no session)
349
+ */
350
+ async saveToGlobal(level) {
351
+ try {
352
+ await fetch(`${CONFIG.API_BASE}/api/app/default-permissions`, {
353
+ method: 'PUT',
354
+ headers: { 'Content-Type': 'application/json' },
355
+ body: JSON.stringify({ permission_level: level })
356
+ });
357
+ this.globalDefault = level;
358
+ } catch (err) {
359
+ console.error('Error saving global permission level:', err);
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Load global defaults from server
365
+ */
366
+ async loadGlobalDefaults() {
367
+ try {
368
+ const response = await fetch(`${CONFIG.API_BASE}/api/app/default-permissions`);
369
+ if (response.ok) {
370
+ const data = await response.json();
371
+ if (Array.isArray(data.modes) && data.modes.length) {
372
+ this.defaultModes = data.modes;
373
+ }
374
+ this._applyModes(this.defaultModes);
375
+ this.globalDefault = this._modeInfo(data.default_level) ? data.default_level : 'dontAsk';
376
+ this.currentLevel = this.globalDefault;
377
+ this.updateButtonState();
378
+ }
379
+ } catch (err) {
380
+ console.error('Error loading global permission defaults:', err);
381
+ this.currentLevel = 'dontAsk';
382
+ this.updateButtonState();
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Update button appearance based on current level
388
+ */
389
+ updateButtonState() {
390
+ if (!this.btn) return;
391
+
392
+ const info = this._modeInfo(this.currentLevel)
393
+ || this._modeInfo('bypassPermissions')
394
+ || this.modes[0];
395
+ this.btn.dataset.level = this.currentLevel;
396
+ // Inline color so engine-specific modes need no per-mode CSS rule
397
+ this.btn.style.color = info.color || '';
398
+ // strings.yaml carries hand-written tooltips for the common modes;
399
+ // engine-specific extras fall back to the template.
400
+ const tooltip = S.permissions.levels[this.currentLevel]?.tooltip
401
+ || S.permissions.tooltip_template
402
+ .replace('{label}', info.label)
403
+ .replace('{desc}', info.desc);
404
+ this.btn.setAttribute('data-tooltip', tooltip);
405
+
406
+ if (this.labelEl) {
407
+ this.labelEl.textContent = info.label;
408
+ }
409
+
410
+ // Set permission color on input container for focus-within border
411
+ const container = document.getElementById('input-container');
412
+ if (container) {
413
+ container.style.setProperty('--permission-color', info.color);
414
+ }
415
+
416
+ // Fresh-session setup panel mirrors this state — keep its pills in
417
+ // sync however the level changed (popup, cycle shortcut, session load).
418
+ window.sessionSetupPanel?.refresh();
419
+ }
420
+
421
+ /**
422
+ * Update which preset appears selected + which one carries the
423
+ * "default" tag (the global default). Called on every popup open.
424
+ */
425
+ updatePresetSelection() {
426
+ const presets = this.popup.querySelectorAll('.permission-preset');
427
+ presets.forEach(preset => {
428
+ preset.classList.toggle('selected', preset.dataset.level === this.currentLevel);
429
+ preset.classList.toggle('is-default', preset.dataset.level === this.globalDefault);
430
+ });
431
+ }
432
+
433
+ /**
434
+ * Get current level key
435
+ */
436
+ getLevel() {
437
+ return this.currentLevel;
438
+ }
439
+
440
+ /**
441
+ * Human label for a mode value (deny explainers, status lines)
442
+ */
443
+ getModeLabel(value) {
444
+ return this._modeInfo(value)?.label || value;
445
+ }
446
+
447
+ /**
448
+ * Get the value to pass to the agent CLI (null for unknown mode)
449
+ */
450
+ getLevelValue() {
451
+ return this._modeInfo(this.currentLevel)?.value ?? null;
452
+ }
453
+
454
+ /**
455
+ * Cycle through permission levels (for keyboard shortcut)
456
+ */
457
+ cycle() {
458
+ const values = this.modes.map(m => m.value);
459
+ const idx = values.indexOf(this.currentLevel);
460
+ this.selectLevel(values[(idx + 1) % values.length]);
461
+ }
462
+ }
463
+
464
+ // Singleton instance
465
+ export const permissionSettings = new PermissionSettingsManager();
466
+
467
+ // Expose globally for config widget and session.js access
468
+ window.permissionSettings = permissionSettings;
469
+
470
+ // Auto-initialize when DOM is ready
471
+ if (document.readyState === 'loading') {
472
+ document.addEventListener('DOMContentLoaded', () => permissionSettings.init());
473
+ } else {
474
+ permissionSettings.init();
475
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Pointer modality — is a real cursor driving the UI right now?
3
+ *
4
+ * CSS cannot answer this on iPadOS. `(hover: none) and (pointer: coarse)`
5
+ * matches whether the user is prodding the glass with a finger or sweeping a
6
+ * Magic Keyboard trackpad cursor: the query describes the device's WEAKEST
7
+ * input, not the one in use. Only runtime pointer events disambiguate — a
8
+ * trackpad reports `pointerType: 'mouse'`, a finger reports `'touch'`.
9
+ *
10
+ * Owns `body.fine-pointer` so CSS can re-enable cursor-only affordances that
11
+ * must stay off for taps (see the shortcut-hint rows in 40-input.css).
12
+ *
13
+ * Deliberately NOT context-menu.js's `lastInputWasTouch()`, for two reasons:
14
+ * - That tracker defaults to 'mouse'. Here that would arm clickable rows
15
+ * before the first touch ever lands, which is the accidental-tap hazard
16
+ * the gate exists to prevent. This one starts unarmed on coarse devices.
17
+ * - It listens only to `pointerdown`. A click's target is resolved from its
18
+ * pointerdown/pointerup targets, so flipping a class at pointerdown is
19
+ * already too late — the press lands on whatever sat underneath. Tracking
20
+ * `pointermove` arms on hover, before any button goes down.
21
+ */
22
+
23
+ const COARSE_QUERY = '(hover: none) and (pointer: coarse)';
24
+
25
+ // Unarmed wherever the device CAN be finger-driven. A desktop with a real
26
+ // mouse matches nothing here and is armed from the first paint.
27
+ let fine = !(typeof matchMedia === 'function' && matchMedia(COARSE_QUERY).matches);
28
+
29
+ function apply() {
30
+ document.body?.classList.toggle('fine-pointer', fine);
31
+ }
32
+
33
+ function note(pointerType) {
34
+ if (!pointerType) return;
35
+ // 'pen' counts as coarse: an Apple Pencil tap carries the same
36
+ // fire-whatever-is-under-the-tip risk a fingertip does.
37
+ const next = pointerType === 'mouse';
38
+ if (next === fine) return;
39
+ fine = next;
40
+ apply();
41
+ }
42
+
43
+ if (typeof document !== 'undefined') {
44
+ // capture+passive: never interferes with anything downstream, and the
45
+ // pointermove flood costs one string compare that early-returns.
46
+ for (const type of ['pointermove', 'pointerdown', 'pointerover']) {
47
+ document.addEventListener(type, (e) => note(e.pointerType), { capture: true, passive: true });
48
+ }
49
+ if (document.readyState === 'loading') {
50
+ document.addEventListener('DOMContentLoaded', apply);
51
+ } else {
52
+ apply();
53
+ }
54
+ }
55
+
56
+ /** True when a real cursor (mouse/trackpad) drove the most recent input. */
57
+ export function hasFinePointer() {
58
+ return fine;
59
+ }