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,108 @@
1
+ /**
2
+ * Preview file-change polling
3
+ *
4
+ * Polls file mtime via /api/file/stat while the preview is visible.
5
+ * - View mode / edit without changes: silent auto-reload
6
+ * - Edit mode with unsaved changes: shows notification bar
7
+ */
8
+
9
+ import { state, fns } from './preview-state.js';
10
+ import { statFile, fetchFile, showToast } from './preview-utils.js';
11
+ import { isInlineEditInProgress } from './preview-inline-edit.js';
12
+ import S from '../strings.js';
13
+
14
+ const POLL_INTERVAL_MS = 5000;
15
+ let pollTimer = null;
16
+
17
+ export function startPolling() {
18
+ if (pollTimer) return;
19
+ pollTimer = setInterval(checkForChanges, POLL_INTERVAL_MS);
20
+ }
21
+
22
+ export function stopPolling() {
23
+ if (pollTimer) {
24
+ clearInterval(pollTimer);
25
+ pollTimer = null;
26
+ }
27
+ }
28
+
29
+ async function checkForChanges() {
30
+ // Skip: no file, scratch mode, loading, or no known mtime
31
+ if (!state.currentPath || state.isScratch || state.isLoading || !state.mtime) return;
32
+
33
+ try {
34
+ const stat = await statFile(state.currentPath);
35
+ if (!stat.exists) return;
36
+ if (stat.mtime <= state.mtime) return;
37
+
38
+ // File changed on disk
39
+ if (state.viewMode === 'edit' && state.modified) {
40
+ showChangedNotification();
41
+ } else if (isInlineEditInProgress()) {
42
+ // An open inline-edit textarea holds unsaved keystrokes; a
43
+ // silentReload() → rerenderContent() would wipe it. Defer — the
44
+ // mtime stays ahead, so we'll reload on a later tick once it closes.
45
+ return;
46
+ } else {
47
+ await silentReload();
48
+ }
49
+ } catch (err) {
50
+ // Network error etc — skip this cycle
51
+ console.warn('[preview-poll] stat failed:', err.message);
52
+ }
53
+ }
54
+
55
+ async function silentReload() {
56
+ try {
57
+ const { content, mtime } = await fetchFile(state.currentPath);
58
+ state.content = content;
59
+ state.mtime = mtime;
60
+ // Update editBuffer if in edit mode (no unsaved changes)
61
+ if (state.viewMode === 'edit') {
62
+ state.editBuffer = content;
63
+ if (state.editor) {
64
+ state.editor.setContent(content);
65
+ }
66
+ }
67
+ fns.rerenderContent();
68
+ } catch (err) {
69
+ console.warn('[preview-poll] reload failed:', err.message);
70
+ }
71
+ }
72
+
73
+ function showChangedNotification() {
74
+ const container = fns.findPreviewContainer();
75
+ if (!container) return;
76
+ // Don't show duplicate
77
+ if (container.querySelector('.preview-changed-bar')) return;
78
+
79
+ const bar = document.createElement('div');
80
+ bar.className = 'preview-changed-bar';
81
+ bar.innerHTML = `
82
+ <span class="preview-changed-text">File changed on disk</span>
83
+ <button class="preview-changed-btn reload">Reload</button>
84
+ <button class="preview-changed-btn dismiss">Dismiss</button>
85
+ `;
86
+
87
+ bar.querySelector('.reload').addEventListener('click', async () => {
88
+ bar.remove();
89
+ await silentReload();
90
+ showToast(S.toast.reloaded);
91
+ });
92
+
93
+ bar.querySelector('.dismiss').addEventListener('click', () => {
94
+ bar.remove();
95
+ // Update mtime so we don't re-trigger until next change
96
+ statFile(state.currentPath).then(s => {
97
+ if (s.exists) state.mtime = s.mtime;
98
+ }).catch(() => {});
99
+ });
100
+
101
+ // Insert at top of the preview body
102
+ const body = container.querySelector('.preview-body');
103
+ if (body) {
104
+ body.insertBefore(bar, body.firstChild);
105
+ } else {
106
+ container.insertBefore(bar, container.firstChild);
107
+ }
108
+ }
@@ -0,0 +1,572 @@
1
+ /**
2
+ * Preview rendering
3
+ *
4
+ * Handles all HTML rendering for the preview widget:
5
+ * renderPreview, renderBody, renderBodyWithState, highlightContent, scrollToLine
6
+ */
7
+
8
+ import { state, pluginHelpers, isEditMode, isHistoryMode, isEditable, wrapLines } from './preview-state.js';
9
+ import { getHighlightLanguage, getEffectiveLanguage, addLineNumbers, AVAILABLE_LANGUAGES } from './preview-utils.js';
10
+ import { escapeHtml } from '../utils.js';
11
+ import { detectLanguage } from '../file-tabs.js';
12
+ import { downloadBtnHtml } from '../preview-plugins/plugin-helpers.js';
13
+ import { highlightCodeToLines } from '../editor-view.js';
14
+ import { renderHistoryBody } from './preview-history.js';
15
+ import { withChords } from '../shortcuts.js';
16
+ import S from '../strings.js';
17
+
18
+ /**
19
+ * Empty state — no file is open. Shown when the preview is summoned on its own
20
+ * (rail button / Alt+V with nothing previewed yet) rather than by a file click,
21
+ * so it has to offer a way IN: the button opens the same path picker as Cmd+O.
22
+ *
23
+ * The key hint is read from the live shortcut registry rather than hardcoded,
24
+ * so it follows a user rebind (and the Mac/other split) the way the rail
25
+ * tooltips do.
26
+ */
27
+ function emptyStateHtml() {
28
+ const key = window.app?.shortcutManager?.getShortcutKeys('openDialog')?.[0];
29
+ const kbd = key ? `<kbd>${escapeHtml(key)}</kbd>` : '';
30
+
31
+ return `
32
+ <div class="preview-body">
33
+ <div class="preview-empty">
34
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
35
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
36
+ <polyline points="14 2 14 8 20 8"/>
37
+ </svg>
38
+ <span>${escapeHtml(S.widgets.file_preview.empty_title)}</span>
39
+ <span class="preview-empty-hint">${escapeHtml(S.widgets.file_preview.empty_hint)}</span>
40
+ <button type="button" class="preview-empty-open" data-action="open-file">
41
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
42
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
43
+ </svg>
44
+ <span>${escapeHtml(S.widgets.file_preview.empty_open)}</span>
45
+ ${kbd}
46
+ </button>
47
+ </div>
48
+ </div>
49
+ `;
50
+ }
51
+
52
+ export function renderPreview() {
53
+ const body = renderBody();
54
+
55
+ return `
56
+ <div class="file-preview-widget">
57
+ ${body}
58
+ </div>
59
+ `;
60
+ }
61
+
62
+ export function renderBody() {
63
+ if (state.isLoading) {
64
+ return `
65
+ <div class="preview-body">
66
+ <div class="preview-loading">
67
+ <div class="loading-spinner"></div>
68
+ <span>Loading...</span>
69
+ </div>
70
+ </div>
71
+ `;
72
+ }
73
+
74
+ if (state.error) {
75
+ return `
76
+ <div class="preview-body">
77
+ <div class="preview-error">
78
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
79
+ <circle cx="12" cy="12" r="10"/>
80
+ <line x1="15" y1="9" x2="9" y2="15"/>
81
+ <line x1="9" y1="9" x2="15" y2="15"/>
82
+ </svg>
83
+ <span>${escapeHtml(state.error)}</span>
84
+ </div>
85
+ </div>
86
+ `;
87
+ }
88
+
89
+ if (!state.currentPath && !state.isScratch) {
90
+ return emptyStateHtml();
91
+ }
92
+
93
+ // Non-editable plugin rendering (image, excalidraw, chart):
94
+ // These return full HTML including their own toolbar, so return directly.
95
+ if (state.plugin && !state.plugin.editable) {
96
+ const pluginHtml = state.plugin.renderBody(state, pluginHelpers);
97
+ if (pluginHtml !== null) return pluginHtml;
98
+ }
99
+
100
+ // View mode toggle — shown for all editable text files (not scratch, which is always edit)
101
+ let viewToggle = '';
102
+ if (isEditable() && !state.isScratch) {
103
+ const tabs = [
104
+ { mode: 'code', label: 'Code', icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>` },
105
+ ];
106
+ // Plugin-defined view modes (e.g. 'rendered' for markdown, 'table' for CSV)
107
+ if (state.plugin?.viewModes) {
108
+ tabs.push(...state.plugin.viewModes);
109
+ }
110
+ tabs.push({ mode: 'edit', label: S.preview?.tab_edit || 'Edit', icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>` });
111
+ // History tab — shadow-git diff browser. Only when we have a backing file path.
112
+ if (state.currentPath) {
113
+ tabs.push({ mode: 'history', label: S.preview?.tab_history || 'History', icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>` });
114
+ }
115
+
116
+ viewToggle = `<div class="preview-view-toggle">${tabs.map(t =>
117
+ `<button class="toggle-btn ${state.viewMode === t.mode ? 'active' : ''} ${t.mode === 'edit' && state.modified ? 'modified' : ''}" data-mode="${t.mode}">${t.icon} ${t.label}</button>`
118
+ ).join('')}</div>`;
119
+ }
120
+
121
+ // Line range badge (shows when viewing specific lines) - clickable to scroll back
122
+ let lineBadge = '';
123
+ if (state.lineRange) {
124
+ const { start, end } = state.lineRange;
125
+ const count = end - start + 1;
126
+ lineBadge = `
127
+ <button class="preview-line-badge" data-scroll-line="${start}" data-tooltip="Scroll to highlighted lines">
128
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
129
+ <path d="M4 6h16M4 12h16M4 18h16"/>
130
+ </svg>
131
+ <span>Lines ${start}\u2013${end}</span>
132
+ <span class="line-count">(${count} line${count > 1 ? 's' : ''})</span>
133
+ </button>
134
+ `;
135
+ } else if (state.highlightLines?.length) {
136
+ const line = state.highlightLines[0];
137
+ lineBadge = `
138
+ <button class="preview-line-badge" data-scroll-line="${line}" data-tooltip="Scroll to highlighted line">
139
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
140
+ <path d="M4 6h16M4 12h16M4 18h16"/>
141
+ </svg>
142
+ <span>Line ${line}</span>
143
+ </button>
144
+ `;
145
+ }
146
+
147
+ // Language selector (code + edit views)
148
+ const effectiveLang = getEffectiveLanguage();
149
+ const isOverridden = state.languageOverride !== null;
150
+ const languageSelector = (!isHistoryMode() && (!state.plugin?.viewModes || state.viewMode === 'code' || state.viewMode === 'edit')) ? `
151
+ <div class="preview-language-selector ${isOverridden ? 'overridden' : ''}">
152
+ <select class="language-select" data-tooltip="Syntax highlighting language">
153
+ ${AVAILABLE_LANGUAGES.map(l =>
154
+ `<option value="${l.value}" ${effectiveLang === l.value ? 'selected' : ''}>${l.label}</option>`
155
+ ).join('')}
156
+ </select>
157
+ ${isOverridden ? `<button class="reset-language-btn" data-tooltip="Reset to auto-detected (${state.language})">×</button>` : ''}
158
+ </div>
159
+ ` : '';
160
+
161
+ // Wrap toggle button (for code and edit views)
162
+ const wrapToggle = (!isHistoryMode() && (!state.plugin?.viewModes || state.viewMode === 'code' || state.viewMode === 'edit')) ? `
163
+ <button class="preview-wrap-toggle ${wrapLines ? 'active' : ''}" data-action="wrap" data-tooltip="${wrapLines ? 'Disable line wrap' : 'Enable line wrap'}">
164
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
165
+ <path d="M3 6h18M3 12h15M3 18h18"/>
166
+ <path d="M19 12v3a2 2 0 0 1-2 2h-2"/>
167
+ <polyline points="13 15 15 17 13 19"/>
168
+ </svg>
169
+ </button>
170
+ ` : '';
171
+
172
+ // Search button (shown for text-based files; hidden in history view).
173
+ // Stays visible while the search bar is open — lit, and re-focusing the
174
+ // existing query rather than reopening a blank search.
175
+ const searchButton = (isEditable() && !isHistoryMode()) ? `
176
+ <button class="preview-search-btn ${state.search.active ? 'active' : ''}" data-action="search" data-tooltip="${withChords(state.search.active ? (S.preview?.search_focus || 'Focus search ({key:Ctrl+F})') : (S.preview?.search_open || 'Search ({key:Ctrl+F})'))}">
177
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
178
+ <circle cx="11" cy="11" r="8"/>
179
+ <line x1="21" y1="21" x2="16.65" y2="16.65"/>
180
+ </svg>
181
+ </button>
182
+ ` : '';
183
+
184
+ // Download link button
185
+ const downloadButton = state.currentPath ? downloadBtnHtml() : '';
186
+
187
+ // Plugin-contributed toolbar controls (e.g. markdown's inline-edit toggle).
188
+ // The plugin decides per viewMode; history view never gets them.
189
+ const pluginControls = (!isHistoryMode() && state.plugin?.renderToolbarControls)
190
+ ? (state.plugin.renderToolbarControls(state, pluginHelpers) || '')
191
+ : '';
192
+
193
+ // Edit-mode search buttons — open the CodeMirror search panel (the code
194
+ // view's own search bar doesn't exist in edit mode). Two entry points:
195
+ // plain search, and search & replace (opens with the replace row expanded).
196
+ const editorSearchButtons = isEditMode() ? `
197
+ <button class="preview-search-btn" data-action="editor-search" data-tooltip="${withChords(S.preview?.editor_search || 'Search ({key:Ctrl+F})')}">
198
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
199
+ <circle cx="11" cy="11" r="8"/>
200
+ <line x1="21" y1="21" x2="16.65" y2="16.65"/>
201
+ </svg>
202
+ </button>
203
+ <button class="preview-search-btn" data-action="editor-replace" data-tooltip="${S.preview?.editor_replace || 'Search & replace'}">
204
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
205
+ <circle cx="10" cy="10" r="7"/>
206
+ <line x1="21" y1="21" x2="15" y2="15"/>
207
+ <path d="M7 10h4"/>
208
+ <polyline points="9.5 7.5 12 10 9.5 12.5"/>
209
+ </svg>
210
+ </button>
211
+ ` : '';
212
+
213
+ // Search bar (shown when search is active)
214
+ const searchBar = state.search.active ? `
215
+ <div class="preview-search-bar">
216
+ <input type="text" class="preview-search-input" placeholder="Search..." value="${escapeHtml(state.search.query)}" autofocus>
217
+ <span class="preview-search-count">${state.search.matches.length > 0 ? `${state.search.currentIndex + 1} of ${state.search.matches.length}` : (state.search.query ? 'No results' : '')}</span>
218
+ <button class="preview-search-nav" data-action="search-prev" data-tooltip="Previous (Shift+Enter)" ${state.search.matches.length === 0 ? 'disabled' : ''}>
219
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
220
+ <polyline points="18 15 12 9 6 15"/>
221
+ </svg>
222
+ </button>
223
+ <button class="preview-search-nav" data-action="search-next" data-tooltip="Next (Enter)" ${state.search.matches.length === 0 ? 'disabled' : ''}>
224
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
225
+ <polyline points="6 9 12 15 18 9"/>
226
+ </svg>
227
+ </button>
228
+ <button class="preview-search-close" data-action="search-close" data-tooltip="Close (Escape)">
229
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
230
+ <line x1="18" y1="6" x2="6" y2="18"/>
231
+ <line x1="6" y1="6" x2="18" y2="18"/>
232
+ </svg>
233
+ </button>
234
+ </div>
235
+ ` : '';
236
+
237
+ // Right-side toolbar controls depend on viewMode
238
+ let rightControls = '';
239
+ if (isEditMode() && state.isScratch) {
240
+ // Scratch mode: language selector + Save As
241
+ rightControls = `
242
+ ${languageSelector}
243
+ ${editorSearchButtons}
244
+ ${wrapToggle}
245
+ <span class="preview-modified-dot ${state.modified ? 'visible' : ''}" data-tooltip="Unsaved changes"></span>
246
+ <button class="preview-edit-btn save-as-btn" data-action="save-as" data-tooltip="Save to file">
247
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
248
+ <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
249
+ <polyline points="17 21 17 13 7 13 7 21"/>
250
+ <polyline points="7 3 7 8 15 8"/>
251
+ </svg>
252
+ Save As
253
+ </button>
254
+ `;
255
+ } else if (isEditMode()) {
256
+ rightControls = `
257
+ ${languageSelector}
258
+ ${editorSearchButtons}
259
+ ${wrapToggle}
260
+ <span class="preview-modified-dot ${state.modified ? 'visible' : ''}" data-tooltip="Unsaved changes"></span>
261
+ <button class="preview-edit-btn save-btn" data-action="edit-save" data-tooltip="Save (Ctrl+S)" ${state.saving ? 'disabled' : ''}>
262
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
263
+ <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
264
+ <polyline points="17 21 17 13 7 13 7 21"/>
265
+ <polyline points="7 3 7 8 15 8"/>
266
+ </svg>
267
+ ${state.saving ? 'Saving...' : 'Save'}
268
+ </button>
269
+ `;
270
+ } else {
271
+ rightControls = `
272
+ ${languageSelector}
273
+ ${pluginControls}
274
+ ${searchButton}
275
+ ${downloadButton}
276
+ ${wrapToggle}
277
+ `;
278
+ }
279
+
280
+ // Unified toolbar: view tabs + right-side controls.
281
+ //
282
+ // An open search bar takes over the LEFT of the row (the view tabs and line
283
+ // badge yield the width to it), but the right-side controls always stay put
284
+ // — an open search is no reason to lose the edit toggle / download / wrap.
285
+ // The spacer is dropped while searching: it and .preview-search-bar are both
286
+ // flex:1, so keeping both would split the row and starve the input.
287
+ const hasToolbar = viewToggle || lineBadge || rightControls || state.search.active;
288
+ const toolbar = hasToolbar ? `
289
+ <div class="preview-toolbar">
290
+ ${state.search.active ? searchBar : ''}
291
+ ${!state.search.active ? viewToggle : ''}
292
+ ${!state.search.active ? lineBadge : ''}
293
+ ${!state.search.active ? '<div class="toolbar-spacer"></div>' : ''}
294
+ ${rightControls}
295
+ </div>
296
+ ` : '';
297
+
298
+ // Edit view (CodeMirror editor)
299
+ if (isEditMode()) {
300
+ return `
301
+ ${toolbar}
302
+ <div class="preview-body preview-edit-body">
303
+ <div class="preview-cm-container">
304
+ <div class="cm-loading">Loading editor...</div>
305
+ </div>
306
+ </div>
307
+ `;
308
+ }
309
+
310
+ // History view (shadow-git diff browser)
311
+ if (isHistoryMode()) {
312
+ return `
313
+ ${toolbar}
314
+ ${renderHistoryBody()}
315
+ `;
316
+ }
317
+
318
+ // Plugin custom view mode (e.g. markdown 'rendered', CSV 'table')
319
+ // Plugin returns HTML for its custom mode, or null to fall through to code view
320
+ if (state.plugin && state.viewMode !== 'code' && state.viewMode !== 'edit') {
321
+ const pluginHtml = state.plugin.renderBody(state, pluginHelpers);
322
+ if (pluginHtml !== null) {
323
+ return `
324
+ ${toolbar}
325
+ ${pluginHtml}
326
+ `;
327
+ }
328
+ }
329
+
330
+ // Text/code content
331
+ const hlLang = getHighlightLanguage(getEffectiveLanguage());
332
+ const linesHtml = addLineNumbers(state.content || '', state.highlightLines, state.lineRange);
333
+ const wrapClass = wrapLines ? ' wrap-lines' : '';
334
+
335
+ return `
336
+ ${toolbar}
337
+ <div class="preview-body">
338
+ <div class="preview-code-wrapper${wrapClass}">
339
+ <pre class="preview-code" data-language="${hlLang}"><code class="language-${hlLang}">${linesHtml}</code></pre>
340
+ </div>
341
+ </div>
342
+ `;
343
+ }
344
+
345
+ /**
346
+ * Render body with a provided state object (for tab-specific rendering)
347
+ */
348
+ export function renderBodyWithState(s) {
349
+ if (s.isLoading) {
350
+ return `
351
+ <div class="preview-body">
352
+ <div class="preview-loading">
353
+ <div class="loading-spinner"></div>
354
+ <span>Loading...</span>
355
+ </div>
356
+ </div>
357
+ `;
358
+ }
359
+
360
+ if (s.error) {
361
+ return `
362
+ <div class="preview-body">
363
+ <div class="preview-error">
364
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
365
+ <circle cx="12" cy="12" r="10"/>
366
+ <line x1="15" y1="9" x2="9" y2="15"/>
367
+ <line x1="9" y1="9" x2="15" y2="15"/>
368
+ </svg>
369
+ <span>${escapeHtml(s.error)}</span>
370
+ </div>
371
+ </div>
372
+ `;
373
+ }
374
+
375
+ if (!s.currentPath) {
376
+ return emptyStateHtml();
377
+ }
378
+
379
+ // Plugin rendering
380
+ if (s.plugin) {
381
+ const pluginHtml = s.plugin.renderBody(s, pluginHelpers);
382
+ if (pluginHtml !== null) return pluginHtml;
383
+ }
384
+
385
+ // Text/code content
386
+ const hlLang = getHighlightLanguage(s.language || detectLanguage(s.currentPath));
387
+ const linesHtml = addLineNumbers(s.content || '', s.highlightLines, s.lineRange);
388
+ const wrapClass = s.wrapLines ? ' wrap-lines' : '';
389
+
390
+ return `
391
+ <div class="preview-body">
392
+ <div class="preview-code-wrapper${wrapClass}">
393
+ <pre class="preview-code" data-language="${hlLang}"><code class="language-${hlLang}">${linesHtml}</code></pre>
394
+ </div>
395
+ </div>
396
+ `;
397
+ }
398
+
399
+ /**
400
+ * Apply syntax highlighting to code content.
401
+ *
402
+ * Primary path: the SAME lezer parser + tagHighlighter the CodeMirror editor
403
+ * uses (highlightCodeToLines from editor-view.js), so the Code view renders
404
+ * identically to Edit mode — one engine, one palette (.tok-* classes in
405
+ * 67-editor-view.css). Falls back to per-line hljs for languages without a
406
+ * CM parser (makefile) and for oversized files. Async: the plain pre-render
407
+ * from renderBody stays visible until the highlighted swap lands.
408
+ */
409
+ export function highlightContent(container) {
410
+ if (!state.content) return;
411
+ if (!container.querySelector('.preview-code-wrapper')) return;
412
+
413
+ const content = state.content;
414
+ const language = getEffectiveLanguage();
415
+
416
+ highlightCodeToLines(content, language).then(result => {
417
+ // Supersede guards: content/language switched while highlighting, the
418
+ // code wrapper re-rendered away, or a search is actively marking lines
419
+ // (closeSearch triggers a fresh rerender + highlight pass anyway).
420
+ const codeWrapper = container.querySelector('.preview-code-wrapper');
421
+ if (!codeWrapper) return;
422
+ if (state.content !== content || getEffectiveLanguage() !== language) return;
423
+ if (codeWrapper.querySelector('.search-match')) return;
424
+
425
+ if (result) {
426
+ renderCodeLines(codeWrapper, result.lines, getHighlightLanguage(language), result.folds);
427
+ } else {
428
+ highlightContentHljs(codeWrapper, content, language);
429
+ }
430
+ });
431
+ }
432
+
433
+ /**
434
+ * Fallback: per-line hljs highlighting (the pre-lezer engine). Kept for
435
+ * languages the CM bundle has no parser for and for very large files.
436
+ */
437
+ function highlightContentHljs(codeWrapper, content, language) {
438
+ if (!window.hljs) return;
439
+ const hlLang = getHighlightLanguage(language);
440
+ const htmlLines = content.split('\n').map(line => {
441
+ try {
442
+ return window.hljs.highlight(line || ' ', { language: hlLang, ignoreIllegals: true }).value;
443
+ } catch {
444
+ return escapeHtml(line) || ' ';
445
+ }
446
+ });
447
+ renderCodeLines(codeWrapper, htmlLines, hlLang);
448
+ }
449
+
450
+ /**
451
+ * Rebuild the code DOM from pre-highlighted per-line HTML, reapplying
452
+ * line-state classes (highlight ranges, flash) from state. `folds` (from the
453
+ * lezer path) renders the same collapse arrows the editor's fold gutter
454
+ * shows, in a fixed-width cell so Code and Edit line up pixel-for-pixel.
455
+ */
456
+ function renderCodeLines(codeWrapper, htmlLines, langLabel, folds) {
457
+ const highlightSet = new Set(state.highlightLines || []);
458
+ const lineRange = state.lineRange;
459
+ // Preserve an in-flight flash animation across the swap (scrollToLine may
460
+ // have landed while we were highlighting)
461
+ const flashed = new Set(
462
+ [...codeWrapper.querySelectorAll('.preview-line.flash')].map(el => el.dataset.line)
463
+ );
464
+ const foldMap = new Map((folds || []).map(f => [f.start, f.end]));
465
+
466
+ const rendered = htmlLines.map((lineHtml, i) => {
467
+ const lineNum = i + 1;
468
+ const classes = ['preview-line'];
469
+ if (highlightSet.has(lineNum)) classes.push('highlighted');
470
+ if (lineRange) {
471
+ if (lineNum === lineRange.start) classes.push('range-start');
472
+ if (lineNum === lineRange.end) classes.push('range-end');
473
+ }
474
+ if (flashed.has(String(lineNum))) classes.push('flash');
475
+ const foldEnd = foldMap.get(lineNum);
476
+ const foldCell = foldEnd
477
+ ? `<span class="preview-fold" data-fold-end="${foldEnd}"></span>`
478
+ : '<span class="preview-fold"></span>';
479
+ return `<div class="${classes.join(' ')}" data-line="${lineNum}"><span class="line-number">${lineNum}</span>${foldCell}<span class="line-content">${lineHtml || ' '}</span></div>`;
480
+ });
481
+
482
+ codeWrapper.innerHTML = `<pre class="preview-code highlighted"><code class="language-${langLabel}">${rendered.join('')}</code></pre>`;
483
+
484
+ // Fold arrow clicks — delegated; wrapper survives highlight swaps within
485
+ // one body render, and re-renders re-run this whole function anyway.
486
+ if (!codeWrapper._foldBound) {
487
+ codeWrapper._foldBound = true;
488
+ codeWrapper.addEventListener('click', handleFoldClick);
489
+ }
490
+ }
491
+
492
+ /**
493
+ * Toggle a fold region: hides/shows the lines inside the range, mirroring
494
+ * the editor's fold gutter behavior. Expanding also resets any nested
495
+ * collapsed folds inside the range.
496
+ */
497
+ function handleFoldClick(e) {
498
+ const btn = e.target.closest('.preview-fold[data-fold-end]');
499
+ if (!btn) return;
500
+ const lineEl = btn.closest('.preview-line');
501
+ if (!lineEl) return;
502
+ const end = parseInt(btn.dataset.foldEnd, 10);
503
+ const collapsed = lineEl.classList.toggle('fold-collapsed');
504
+ for (let el = lineEl.nextElementSibling; el; el = el.nextElementSibling) {
505
+ const n = parseInt(el.dataset.line, 10);
506
+ if (isNaN(n) || n > end) break;
507
+ el.classList.toggle('fold-hidden', collapsed);
508
+ if (!collapsed) el.classList.remove('fold-collapsed');
509
+ }
510
+ }
511
+
512
+ /**
513
+ * Scroll to a specific line
514
+ * @param {Element} container - Widget container
515
+ * @param {number} lineNum - Line number to scroll to
516
+ * @param {object} options - Options
517
+ * @param {boolean} options.flash - Add flash animation (default: true)
518
+ * @param {string} options.position - 'center' or 'top' (default: 'center')
519
+ */
520
+ export function scrollToLine(container, lineNum, options = {}) {
521
+ const { flash = true, position = 'center' } = options;
522
+ // Code/edit views: line-marked DOM. Rendered views (e.g. markdown):
523
+ // map line → source byte offset → block tagged with data-source-start/end.
524
+ const target = container.querySelector(`.preview-line[data-line="${lineNum}"]`)
525
+ || findRenderedBlockForLine(container, lineNum);
526
+ if (!target) return;
527
+
528
+ const scrollContainer = container.querySelector('.preview-body');
529
+
530
+ if (position === 'top' && scrollContainer) {
531
+ const containerRect = scrollContainer.getBoundingClientRect();
532
+ const lineRect = target.getBoundingClientRect();
533
+ const currentScrollTop = scrollContainer.scrollTop;
534
+ const lineOffset = lineRect.top - containerRect.top + currentScrollTop;
535
+ scrollContainer.scrollTop = lineOffset;
536
+ } else {
537
+ target.scrollIntoView({ behavior: 'smooth', block: 'center' });
538
+ }
539
+
540
+ if (flash) {
541
+ target.classList.add('flash');
542
+ setTimeout(() => target.classList.remove('flash'), 1500);
543
+ }
544
+ }
545
+
546
+ function findRenderedBlockForLine(container, lineNum) {
547
+ const rendered = container.querySelector('.preview-rendered');
548
+ if (!rendered) return null;
549
+
550
+ const content = state.content || '';
551
+ let offset = 0;
552
+ let line = 1;
553
+ while (line < lineNum && offset < content.length) {
554
+ const nl = content.indexOf('\n', offset);
555
+ if (nl === -1) break;
556
+ offset = nl + 1;
557
+ line++;
558
+ }
559
+
560
+ let best = null;
561
+ let bestStart = -1;
562
+ for (const el of rendered.querySelectorAll('[data-source-start]')) {
563
+ const start = parseInt(el.getAttribute('data-source-start'), 10);
564
+ const end = parseInt(el.getAttribute('data-source-end'), 10);
565
+ if (offset >= start && offset <= end) return el;
566
+ if (start <= offset && start > bestStart) {
567
+ best = el;
568
+ bestStart = start;
569
+ }
570
+ }
571
+ return best;
572
+ }