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,861 @@
1
+ /**
2
+ * FilePreviewWidget - File preview and editor
3
+ *
4
+ * Orchestrator: imports modules, wires cross-module deps, registers widget, exposes public API.
5
+ *
6
+ * Entry points:
7
+ * - File explorer, chat file links, search results
8
+ * - Tool output "Open in editor" buttons
9
+ */
10
+
11
+ import { WidgetManager, WidgetBus } from '../widget-system/index.js';
12
+ import { OpenDialog } from '../open-dialog.js';
13
+ import { detectLanguage } from '../file-tabs.js';
14
+ import { fileDownloadAction } from '../context-menu.js';
15
+ import { findPlugin } from '../preview-plugins/index.js';
16
+
17
+ // Preview modules
18
+ import {
19
+ state, activateState, removeTabState, removeSessionState,
20
+ isEditMode, isEditable, pluginHelpers,
21
+ wrapLines, setWrapLines, WRAP_STORAGE_KEY, PATH_STORAGE_KEY,
22
+ rememberLastPath, getLastPath, recallScroll, forgetScroll,
23
+ fns
24
+ } from '../preview/preview-state.js';
25
+ import {
26
+ getRelativePath, fetchFile, showToast,
27
+ DEFAULT_WIDTH, DEFAULT_HEIGHT, loadSavedSize, saveSize, resetSize
28
+ } from '../preview/preview-utils.js';
29
+ import { startPolling, stopPolling } from '../preview/preview-poll.js';
30
+ import { renderPreview, renderBody, highlightContent, scrollToLine } from '../preview/preview-render.js';
31
+ import { rerenderContent, openSearch, closeSearch, performSearch, navigateSearch } from '../preview/preview-search.js';
32
+ import { switchToEditView, leaveEditView, discardEdits, saveFile, saveAsFile } from '../preview/preview-edit.js';
33
+ import { setupEventHandlers } from '../preview/preview-events.js';
34
+ import { renderBreadcrumb, closeMenu as closeBreadcrumbMenu } from '../preview/preview-breadcrumb.js';
35
+ import S from '../strings.js';
36
+ import { appConfirm } from '../utils.js';
37
+ import { debug } from '../config.js';
38
+ import { basename } from '../path-utils.js';
39
+
40
+ // ═══════════════════════════════════════════════════════════════════════════
41
+ // MODULE SETUP
42
+ // ═══════════════════════════════════════════════════════════════════════════
43
+
44
+ let widget = null;
45
+ const MODULE_ID = Math.random().toString(36).substr(2, 6);
46
+ debug.log('[FilePreviewWidget] Module loaded, ID:', MODULE_ID);
47
+
48
+ // ═══════════════════════════════════════════════════════════════════════════
49
+ // ORCHESTRATOR-ONLY FUNCTIONS
50
+ // ═══════════════════════════════════════════════════════════════════════════
51
+
52
+ function updateWidgetHeader() {
53
+ // Walk from the per-session content container to the outer widget div.
54
+ // Using getElementById('widget-file-preview') would miss session-scoped
55
+ // widgets, whose DOM IDs are widget-file-preview-${sessionId}.
56
+ const widgetEl = state.container?.closest('.widget');
57
+ if (!widgetEl) return;
58
+
59
+ const titleEl = widgetEl.querySelector('.widget-title');
60
+ if (!titleEl) return;
61
+
62
+ if (state.currentPath && !state.isScratch) {
63
+ // Interactive breadcrumb — click a segment to browse siblings / switch files.
64
+ renderBreadcrumb(titleEl);
65
+ titleEl.setAttribute('data-tooltip', state.currentPath);
66
+ } else {
67
+ closeBreadcrumbMenu();
68
+ titleEl.classList.remove('has-breadcrumb');
69
+ titleEl.textContent = '';
70
+ titleEl.removeAttribute('data-tooltip');
71
+ }
72
+ }
73
+
74
+ async function copyPath() {
75
+ if (!state.currentPath) return;
76
+ try {
77
+ await navigator.clipboard.writeText(state.currentPath);
78
+ showToast(S.toast.path_copied);
79
+ } catch (e) {
80
+ console.error('Failed to copy path:', e);
81
+ }
82
+ }
83
+
84
+ async function copyContent() {
85
+ if (!state.content) return;
86
+ try {
87
+ await navigator.clipboard.writeText(state.content);
88
+ showToast(S.toast.content_copied);
89
+ } catch (e) {
90
+ console.error('Failed to copy content:', e);
91
+ }
92
+ }
93
+
94
+ async function handleDownload() {
95
+ if (!state.currentPath) return;
96
+ await fileDownloadAction(state.currentPath);
97
+ }
98
+
99
+ async function revealInFolder() {
100
+ if (!state.currentPath) return;
101
+ const explorer = window.FileExplorerWidget;
102
+ if (!explorer?.revealFile) {
103
+ showToast(S.toast.reveal_failed.replace('{error}', 'File Explorer not available'));
104
+ return;
105
+ }
106
+ await explorer.revealFile(state.currentPath);
107
+ }
108
+
109
+ /**
110
+ * Find the active preview widget container (floating, tab, or fallback)
111
+ */
112
+ function findPreviewContainer() {
113
+ // Per-instance: use state's stored container reference
114
+ if (state.container) {
115
+ return state.container.querySelector('.file-preview-widget') || state.container;
116
+ }
117
+ // Floating/bottom-sheet/sidebar
118
+ if (WidgetManager.isOpen('file-preview')) {
119
+ return document.querySelector('.widget[data-widget-id="file-preview"] .file-preview-widget');
120
+ }
121
+ // Tab mode or fallback
122
+ return document.querySelector('.widget-tab-content .file-preview-widget')
123
+ || document.querySelector('.file-preview-widget');
124
+ }
125
+
126
+ function openPreviewAsTab({ background } = {}) {
127
+ const currentPath = state.currentPath;
128
+ WidgetBus.emit('widget:open-as-tab', {
129
+ widgetId: 'file-preview',
130
+ title: state.currentPath ? getRelativePath(state.currentPath, state.cwd) : 'File Preview',
131
+ icon: 'file',
132
+ filePath: currentPath,
133
+ background
134
+ });
135
+ }
136
+
137
+ function toggleViewMode() {
138
+ if (!state.plugin?.viewModes?.length) return;
139
+ const customMode = state.plugin.viewModes[0].mode;
140
+ state.viewMode = state.viewMode === 'code' ? customMode : 'code';
141
+ WidgetManager.update('file-preview');
142
+ }
143
+
144
+ function toggleWrapLines() {
145
+ const container = document.querySelector('.file-preview-widget');
146
+ const scrollContainer = container?.querySelector('.preview-body');
147
+ let visibleLine = null;
148
+
149
+ if (scrollContainer) {
150
+ const scrollTop = scrollContainer.scrollTop;
151
+ const lineRows = scrollContainer.querySelectorAll('.preview-line');
152
+
153
+ for (const row of lineRows) {
154
+ const rowTop = row.getBoundingClientRect().top - scrollContainer.getBoundingClientRect().top + scrollTop;
155
+ if (rowTop + row.offsetHeight > scrollTop) {
156
+ visibleLine = parseInt(row.dataset.line, 10);
157
+ break;
158
+ }
159
+ }
160
+ }
161
+
162
+ setWrapLines(!wrapLines);
163
+ try {
164
+ localStorage.setItem(WRAP_STORAGE_KEY, wrapLines ? 'true' : 'false');
165
+ } catch (e) { /* ignore */ }
166
+
167
+ // Live-reconfigure CodeMirror in edit mode (preserves cursor + unsaved buffer)
168
+ if (isEditMode() && state.editor?.setLineWrapping) {
169
+ state.editor.setLineWrapping(wrapLines);
170
+ // Update only the toggle button's visual state — avoid full re-render
171
+ const wrapBtn = container?.querySelector('.preview-wrap-toggle');
172
+ if (wrapBtn) {
173
+ wrapBtn.classList.toggle('active', wrapLines);
174
+ wrapBtn.setAttribute('data-tooltip', wrapLines ? 'Disable line wrap' : 'Enable line wrap');
175
+ }
176
+ return;
177
+ }
178
+
179
+ if (visibleLine) {
180
+ state.scrollToLine = visibleLine;
181
+ state.scrollOptions = { flash: false, position: 'top' };
182
+ }
183
+
184
+ WidgetManager.update('file-preview');
185
+ }
186
+
187
+ /**
188
+ * Empty-state "Open a file…" action — deliberately the same picker as Cmd+O
189
+ * rather than the file explorer, so there's one way to reach for a file by
190
+ * name. Opening a file from it routes back through previewFile().
191
+ */
192
+ function openFileDialog() {
193
+ OpenDialog.show();
194
+ }
195
+
196
+ function closeWidget() {
197
+ try {
198
+ localStorage.removeItem(PATH_STORAGE_KEY);
199
+ } catch (e) { /* ignore */ }
200
+ WidgetManager.close('file-preview');
201
+ }
202
+
203
+ // ═══════════════════════════════════════════════════════════════════════════
204
+ // WIRE CROSS-MODULE DEPENDENCIES
205
+ // ═══════════════════════════════════════════════════════════════════════════
206
+
207
+ Object.assign(fns, {
208
+ // From preview-render.js
209
+ renderBody, renderPreview, highlightContent, scrollToLine,
210
+ // From preview-search.js
211
+ rerenderContent, openSearch, closeSearch, performSearch, navigateSearch,
212
+ // From preview-edit.js
213
+ switchToEditView, leaveEditView, discardEdits, saveFile, saveAsFile,
214
+ // From preview-events.js
215
+ setupEventHandlers,
216
+ // Orchestrator-only
217
+ toggleWrapLines, openPreviewAsTab, handleDownload,
218
+ findPreviewContainer, closeWidget, openFileDialog,
219
+ // Breadcrumb fallback when window.app.previewFile is unavailable
220
+ openPreviewPath: (p) => FilePreviewWidget.preview(p),
221
+ pluginHelpers,
222
+ });
223
+
224
+ // ═══════════════════════════════════════════════════════════════════════════
225
+ // WIDGET REGISTRATION
226
+ // ═══════════════════════════════════════════════════════════════════════════
227
+
228
+ export function registerFilePreviewWidget() {
229
+ debug.log('[FilePreviewWidget] registerFilePreviewWidget() called, MODULE_ID:', MODULE_ID);
230
+ const savedSize = loadSavedSize();
231
+
232
+ const ICONS = {
233
+ copyPath: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
234
+ <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
235
+ <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
236
+ </svg>`,
237
+ copyContent: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
238
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
239
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
240
+ </svg>`,
241
+ edit: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
242
+ <path d="M12 20h9"/>
243
+ <path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
244
+ </svg>`,
245
+ download: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
246
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
247
+ <polyline points="7 10 12 15 17 10"/>
248
+ <line x1="12" y1="15" x2="12" y2="3"/>
249
+ </svg>`,
250
+ revealInFolder: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
251
+ <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"/>
252
+ <circle cx="12" cy="14" r="2.5"/>
253
+ </svg>`
254
+ };
255
+
256
+ widget = WidgetManager.register('file-preview', {
257
+ title: '',
258
+ icon: 'file',
259
+ type: 'floating',
260
+ hiddenInPicker: true,
261
+ closeable: true,
262
+ resizable: true,
263
+ minSize: { width: 400, height: 300 },
264
+ size: { width: savedSize.width, height: savedSize.height },
265
+ // Explicit position rather than the top-right default: the file
266
+ // explorer itself spawns top-right and preview is nearly always
267
+ // opened from it, so offsetting left keeps both readable at once.
268
+ // User-dragged position still persists.
269
+ position: { x: 460, y: 80 },
270
+
271
+ deviceTypes: {
272
+ phone: 'bottom-sheet',
273
+ tablet: 'bottom-sheet',
274
+ desktop: 'floating'
275
+ },
276
+
277
+ allowedTypes: ['floating', 'bottom-sheet', 'sidebar-right', 'tab'],
278
+
279
+ onOpenAsTab: openPreviewAsTab,
280
+
281
+ headerActions: [
282
+ { icon: ICONS.copyPath, title: S.widgets.header_actions.copy_path, onClick: copyPath },
283
+ { icon: ICONS.revealInFolder, title: S.widgets.header_actions.reveal_in_folder, onClick: revealInFolder },
284
+ { icon: '<span class="header-separator"></span>', title: '', onClick: () => {} },
285
+ { icon: ICONS.copyContent, title: S.widgets.header_actions.copy_content, onClick: copyContent },
286
+ ],
287
+
288
+ onResize(dimensions) {
289
+ saveSize(dimensions.width, dimensions.height);
290
+ },
291
+
292
+ beforeClose() {
293
+ activateState(null);
294
+ if (state.viewMode === 'edit' && state.modified) {
295
+ // beforeClose is a sync contract — block the close now,
296
+ // confirm asynchronously, then re-close with the flag cleared.
297
+ appConfirm(S.widgets.file_preview.discard_confirm, { confirmLabel: 'Discard', danger: true })
298
+ .then(ok => {
299
+ if (!ok) return;
300
+ state.modified = false;
301
+ WidgetManager.close('file-preview');
302
+ });
303
+ return false;
304
+ }
305
+ return true;
306
+ },
307
+
308
+ onClose() {
309
+ stopPolling();
310
+ closeBreadcrumbMenu();
311
+ activateState(null);
312
+ if (state.editor) {
313
+ state.editor.destroy();
314
+ state.editor = null;
315
+ }
316
+ if (state.viewMode === 'edit') {
317
+ state.editBuffer = null;
318
+ state.modified = false;
319
+ state.viewMode = 'code';
320
+ }
321
+ state.container = null;
322
+ try {
323
+ localStorage.removeItem(PATH_STORAGE_KEY);
324
+ } catch (e) { /* ignore */ }
325
+ },
326
+
327
+ onDestroy(sessionId) {
328
+ // Called when the owning session tab is closed — release the
329
+ // per-session PreviewState so it doesn't leak.
330
+ removeSessionState(sessionId);
331
+ },
332
+
333
+ render(container, context) {
334
+ const tabId = (context?.isTab && context?.tabId) ? context.tabId : null;
335
+ activateState(tabId);
336
+ state.container = container;
337
+
338
+ // Scratch mode: editable pad with no backing file
339
+ if (context?.isScratch) {
340
+ let saved = null;
341
+ try {
342
+ const raw = localStorage.getItem(`claude-scratch-${context.scratchId}`);
343
+ if (raw) saved = JSON.parse(raw);
344
+ } catch (e) { /* ignore */ }
345
+
346
+ state.isScratch = true;
347
+ state.scratchId = context.scratchId;
348
+ state.currentPath = null;
349
+ state.content = saved?.content || '';
350
+ state.editBuffer = state.content;
351
+ state.language = saved?.language || 'text';
352
+ state.languageOverride = saved?.language || null;
353
+ if (context.cwd) state.cwd = context.cwd;
354
+ state.viewMode = 'edit';
355
+ state.isLoading = false;
356
+ state.error = null;
357
+ state.plugin = null;
358
+ state.pluginState = {};
359
+ state.modified = false;
360
+ state.search.active = false;
361
+
362
+ container.innerHTML = renderPreview();
363
+ setupEventHandlers(container);
364
+
365
+ const previewEl = container.querySelector('.file-preview-widget') || container;
366
+ switchToEditView(previewEl);
367
+ return;
368
+ }
369
+
370
+ // Clear scratch state when rendering a file
371
+ state.isScratch = false;
372
+ state.scratchId = null;
373
+
374
+ if (context?.filePath) {
375
+ const needsFetch = !state.content || state.currentPath !== context.filePath;
376
+ state.currentPath = context.filePath;
377
+ state.plugin = findPlugin(context.filePath);
378
+ state.pluginState = state.plugin?.initState() || {};
379
+ state.language = detectLanguage(context.filePath);
380
+ state.viewMode = state.plugin?.defaultViewMode || 'code';
381
+
382
+ state.search.active = false;
383
+ state.search.query = '';
384
+ state.search.matches = [];
385
+ state.search.currentIndex = -1;
386
+
387
+ const shouldFetch = state.plugin ? state.plugin.needsFetch : true;
388
+ if (needsFetch && shouldFetch) {
389
+ state.isLoading = true;
390
+ state.content = null;
391
+ state.error = null;
392
+
393
+ const containerId = container.id;
394
+ const isTab = context.isTab;
395
+ const capturedTabId = tabId;
396
+ const filePath = context.filePath;
397
+
398
+ debug.log('[FilePreview] Starting fetch for:', context.filePath, isTab ? '(tab)' : '(floating)');
399
+ fetchFile(context.filePath).then(({ content, mtime }) => {
400
+ debug.log('[FilePreview] Fetch complete, content length:', content?.length, 'for:', filePath);
401
+
402
+ // Re-activate correct state — global `state` may have switched
403
+ // (e.g., setSession() → render() → activateState(null) during init)
404
+ if (capturedTabId) activateState(capturedTabId);
405
+
406
+ if (state.currentPath === filePath) {
407
+ state.content = content;
408
+ state.mtime = mtime;
409
+ state.isLoading = false;
410
+ }
411
+
412
+ if (isTab && containerId) {
413
+ const tabContainer = document.getElementById(containerId);
414
+ if (tabContainer) {
415
+ const w = tabContainer.querySelector('.file-preview-widget');
416
+ if (w) {
417
+ w.innerHTML = renderBody();
418
+ setupEventHandlers(w);
419
+ if (content) {
420
+ highlightContent(w);
421
+ }
422
+ // This path bypasses render()/rerenderContent(),
423
+ // so it needs its own scroll restore.
424
+ if (!state.scrollToLine) {
425
+ const remembered = recallScroll(state.currentPath, state.viewMode);
426
+ if (remembered) {
427
+ requestAnimationFrame(() => requestAnimationFrame(() => {
428
+ const body = w.querySelector('.preview-body');
429
+ if (body) body.scrollTop = remembered;
430
+ }));
431
+ }
432
+ }
433
+ debug.log('[FilePreview] Tab container updated directly');
434
+ return;
435
+ }
436
+ }
437
+ }
438
+ rerenderContent();
439
+ }).catch(err => {
440
+ console.error('[FilePreview] Fetch error:', err);
441
+
442
+ if (capturedTabId) activateState(capturedTabId);
443
+
444
+ if (state.currentPath === filePath) {
445
+ state.error = err.message || 'Failed to load file';
446
+ state.isLoading = false;
447
+ }
448
+
449
+ if (isTab && containerId) {
450
+ const tabContainer = document.getElementById(containerId);
451
+ if (tabContainer) {
452
+ const w = tabContainer.querySelector('.file-preview-widget');
453
+ if (w) {
454
+ w.innerHTML = renderBody();
455
+ setupEventHandlers(w);
456
+ debug.log('[FilePreview] Tab container updated with error');
457
+ return;
458
+ }
459
+ }
460
+ }
461
+ rerenderContent();
462
+ });
463
+ } else if (!shouldFetch) {
464
+ state.isLoading = false;
465
+ }
466
+ }
467
+
468
+ // If state has a currentPath + isLoading but no filePath was passed in context,
469
+ // this is a restore from localStorage (module-level init set isLoading=true).
470
+ // The onMount callback was meant to handle this but is never called by the widget system.
471
+ // Initiate the fetch here instead.
472
+ if (!context?.filePath && state.currentPath && state.isLoading && !state.content) {
473
+ const shouldFetch = state.plugin ? state.plugin.needsFetch : true;
474
+ if (shouldFetch) {
475
+ const filePath = state.currentPath;
476
+ fetchFile(filePath)
477
+ .then(({ content, mtime }) => {
478
+ if (state.currentPath === filePath) {
479
+ state.content = content;
480
+ state.mtime = mtime;
481
+ state.isLoading = false;
482
+ }
483
+ rerenderContent();
484
+ })
485
+ .catch(error => {
486
+ if (state.currentPath === filePath) {
487
+ state.error = error.message;
488
+ state.isLoading = false;
489
+ }
490
+ rerenderContent();
491
+ });
492
+ } else {
493
+ state.isLoading = false;
494
+ }
495
+ }
496
+
497
+ debug.log('[FilePreviewWidget] render called, state:', JSON.stringify({
498
+ currentPath: state.currentPath,
499
+ hasContent: !!state.content,
500
+ plugin: state.plugin?.id || null,
501
+ isLoading: state.isLoading,
502
+ error: state.error
503
+ }));
504
+ container.innerHTML = renderPreview();
505
+ setupEventHandlers(container);
506
+
507
+ // No `!state.plugin` guard: a plugin owning the file says nothing
508
+ // about which view is on screen, and every plugin-backed type
509
+ // (.json, .csv, .md, .html, .jsonl) still has a Code tab. That
510
+ // guard is why JSON rendered all-white in Code view. highlightContent
511
+ // is already a no-op when there's no .preview-code-wrapper, so the
512
+ // view-mode check lives in exactly one place.
513
+ if (state.content) {
514
+ highlightContent(container);
515
+ }
516
+
517
+ if (state.scrollToLine && state.content && !state.isLoading) {
518
+ const pendingLine = state.scrollToLine;
519
+ const pendingOptions = state.scrollOptions || {};
520
+ state.scrollToLine = null;
521
+ state.scrollOptions = null;
522
+ // Double-rAF: first for layout after innerHTML, second to ensure scroll target dimensions
523
+ requestAnimationFrame(() => requestAnimationFrame(() => {
524
+ scrollToLine(container, pendingLine, pendingOptions);
525
+ }));
526
+ } else if (state.content && !state.isLoading && !isEditMode()) {
527
+ // No explicit line target — restore where this file was left.
528
+ // Deliberately the `else` branch: an explicit line request
529
+ // (chat file link, search result) always outranks the memory.
530
+ const remembered = recallScroll(state.currentPath, state.viewMode);
531
+ if (remembered) {
532
+ requestAnimationFrame(() => requestAnimationFrame(() => {
533
+ const body = container.querySelector('.preview-body');
534
+ if (body) body.scrollTop = remembered;
535
+ }));
536
+ }
537
+ }
538
+
539
+ updateWidgetHeader();
540
+ },
541
+
542
+ onMount(container) {
543
+ // Preload CodeMirror in background
544
+ import('../editor-view.js').then(m => m.preloadCodeMirror()).catch(() => {});
545
+
546
+ const shouldFetch = state.plugin ? state.plugin.needsFetch : true;
547
+ if (state.currentPath && state.isLoading && !state.content && shouldFetch) {
548
+ fetchFile(state.currentPath)
549
+ .then(({ content, mtime }) => {
550
+ state.content = content;
551
+ state.mtime = mtime;
552
+ state.isLoading = false;
553
+ rerenderContent();
554
+ })
555
+ .catch(error => {
556
+ state.error = error.message;
557
+ state.isLoading = false;
558
+ rerenderContent();
559
+ });
560
+ } else if (!shouldFetch && state.isLoading) {
561
+ state.isLoading = false;
562
+ rerenderContent();
563
+ }
564
+
565
+ startPolling();
566
+ },
567
+
568
+ onUnmount() {
569
+ stopPolling();
570
+ }
571
+
572
+ // Note: ESC key handling is centralized in app.handleEscape()
573
+ });
574
+
575
+ // Listen for session changes — re-point state to the new session's instance,
576
+ // then propagate CWD. Without re-activating, event handlers on a hidden widget
577
+ // would still mutate the previously-active session's state.
578
+ WidgetBus.on('session:changed', ({ cwd }) => {
579
+ activateState(null);
580
+ if (cwd) state.cwd = cwd;
581
+ });
582
+
583
+ // Swap per-instance state when switching between widget tabs
584
+ WidgetBus.on('widget:tab-activated', ({ widgetId, tabId }) => {
585
+ if (widgetId === 'file-preview') {
586
+ activateState(tabId);
587
+ if (state.editor) {
588
+ requestAnimationFrame(() => state.editor.focus());
589
+ }
590
+ }
591
+ });
592
+
593
+ debug.log('[FilePreviewWidget] Registered');
594
+ }
595
+
596
+ // ═══════════════════════════════════════════════════════════════════════════
597
+ // PUBLIC API
598
+ // ═══════════════════════════════════════════════════════════════════════════
599
+
600
+ export const FilePreviewWidget = {
601
+ /**
602
+ * Preview a file
603
+ * @param {string} path - File path to preview
604
+ * @param {object} options - Options
605
+ * @param {number} options.line - Single line to scroll to and highlight
606
+ * @param {number} options.start - Start of line range to highlight
607
+ * @param {number} options.end - End of line range to highlight
608
+ * @param {number[]} options.highlightLines - Specific lines to highlight
609
+ */
610
+ async preview(path, options = {}) {
611
+ debug.log('[FilePreviewWidget] preview() called with path:', path, 'options:', options);
612
+
613
+ activateState(null);
614
+
615
+ state.isScratch = false;
616
+ state.scratchId = null;
617
+
618
+ if (state.modified && path !== state.currentPath) {
619
+ const ok = await appConfirm(S.widgets.file_preview.discard_confirm, { confirmLabel: 'Discard', danger: true });
620
+ if (!ok) return;
621
+ }
622
+
623
+ if (path !== state.currentPath) {
624
+ if (state.editor) {
625
+ state.editor.destroy();
626
+ state.editor = null;
627
+ }
628
+ discardEdits();
629
+ state.viewMode = 'code';
630
+ }
631
+
632
+ state.search.active = false;
633
+ state.search.query = '';
634
+ state.search.matches = [];
635
+ state.search.currentIndex = -1;
636
+
637
+ state.currentPath = path;
638
+ state.plugin = findPlugin(path);
639
+ state.pluginState = state.plugin?.initState() || {};
640
+
641
+ try {
642
+ localStorage.setItem(PATH_STORAGE_KEY, path);
643
+ } catch (e) { /* ignore */ }
644
+ // Survives close (unlike PATH_STORAGE_KEY) so toggle() can reopen it.
645
+ rememberLastPath(path);
646
+
647
+ WidgetBus.emit('widget:file-changed', {
648
+ widgetId: 'file-preview',
649
+ filePath: path,
650
+ fileName: basename(path)
651
+ });
652
+
653
+ state.language = detectLanguage(path);
654
+ state.languageOverride = null;
655
+ state.isLoading = true;
656
+ state.error = null;
657
+ state.content = null;
658
+
659
+ if (options.start && options.end) {
660
+ state.lineRange = { start: options.start, end: options.end };
661
+ const lines = [];
662
+ for (let i = options.start; i <= options.end; i++) {
663
+ lines.push(i);
664
+ }
665
+ state.highlightLines = lines;
666
+ state.scrollToLine = options.start;
667
+ } else if (options.line) {
668
+ state.lineRange = null;
669
+ state.scrollToLine = options.line;
670
+ state.highlightLines = options.highlightLines || [options.line];
671
+ } else {
672
+ state.lineRange = null;
673
+ state.scrollToLine = null;
674
+ state.highlightLines = options.highlightLines || null;
675
+ }
676
+
677
+ // An explicit line target supersedes the remembered position. Drop the
678
+ // memory rather than just letting scrollToLine win once: loading a file
679
+ // renders several times (loading → fetch settle → update), scrollToLine
680
+ // is consumed on the first of those, and a surviving memory entry would
681
+ // yank the view back off the requested line on the next render. The
682
+ // scroll listener re-records wherever the line nav lands.
683
+ if (state.scrollToLine) forgetScroll(path);
684
+
685
+ const pluginDefault = state.plugin?.defaultViewMode;
686
+ state.viewMode = (pluginDefault && !state.scrollToLine) ? pluginDefault : 'code';
687
+
688
+ WidgetBus.emit('widget:close-tab', { widgetId: 'file-preview', keepScratch: true });
689
+
690
+ WidgetManager.open('file-preview', { filePath: path });
691
+
692
+ const shouldFetch = state.plugin ? state.plugin.needsFetch : true;
693
+ if (shouldFetch) {
694
+ try {
695
+ const result = await fetchFile(path);
696
+ state.content = result.content;
697
+ state.mtime = result.mtime;
698
+ state.isLoading = false;
699
+ } catch (error) {
700
+ state.error = error.message;
701
+ state.isLoading = false;
702
+ }
703
+ } else {
704
+ state.isLoading = false;
705
+ }
706
+
707
+ startPolling();
708
+ WidgetManager.update('file-preview');
709
+
710
+ if (options.edit && state.content && isEditable()) {
711
+ await switchToEditView();
712
+ }
713
+ },
714
+
715
+ /**
716
+ * Preview a file and open it directly in the History tab. Optionally seeds
717
+ * From/To cursors so the diff lands on a specific shadow snapshot pair
718
+ * instead of the default (newest snapshot vs auto-prev).
719
+ *
720
+ * @param {string} path
721
+ * @param {object} options
722
+ * @param {object} [options.seed] - { fromKind, fromHash, toKind, toHash }
723
+ */
724
+ async previewWithHistory(path, options = {}) {
725
+ activateState(null);
726
+ // Always seed so a repeat click on the same file resets cursors.
727
+ // Default seed = same as preview-history's "fresh load" default.
728
+ state.historyPendingSeed = options.seed || { toKind: 'snapshot', fromKind: 'auto' };
729
+ await this.preview(path, options);
730
+ // preview() resets viewMode to 'code' on path change — flip after.
731
+ state.viewMode = 'history';
732
+ WidgetManager.update('file-preview');
733
+ },
734
+
735
+ close: closeWidget,
736
+
737
+ /**
738
+ * Show/hide the preview, reopening the last file when it was closed.
739
+ *
740
+ * Unlike WidgetManager.toggle('file-preview'), this restores content: the
741
+ * widget's onClose() tears down the editor and drops the persisted path, so
742
+ * a bare re-open would render an empty shell. Resolution order:
743
+ * 1. the active session's in-memory currentPath (survives Escape)
744
+ * 2. the last-previewed path from localStorage (survives reload)
745
+ * 3. nothing to restore → the empty state, which offers the Cmd+O picker
746
+ *
747
+ * Uses isShowing() rather than isOpen() so a preview hidden by a session
748
+ * switch re-reveals instead of toggling itself further off.
749
+ */
750
+ async toggle() {
751
+ if (WidgetManager.isShowing('file-preview')) {
752
+ closeWidget();
753
+ return;
754
+ }
755
+
756
+ activateState(null);
757
+ const path = state.currentPath || getLastPath();
758
+ if (path) {
759
+ await this.preview(path);
760
+ return;
761
+ }
762
+
763
+ // Nothing to restore: show the widget anyway rather than swallowing the
764
+ // click. Its empty state carries the "Open a file…" button, so the user
765
+ // lands somewhere with an obvious next step instead of on a no-op.
766
+ //
767
+ // update() is required, not belt-and-braces: open() only re-renders when
768
+ // handed a context, so an already-built widget would keep whatever DOM
769
+ // it had last — showing a stale file that state no longer points at.
770
+ WidgetManager.open('file-preview');
771
+ WidgetManager.update('file-preview');
772
+ },
773
+
774
+ isOpen() {
775
+ if (WidgetManager.isOpen('file-preview')) {
776
+ return true;
777
+ }
778
+ const container = document.querySelector('.file-preview-widget');
779
+ return container && container.offsetParent !== null;
780
+ },
781
+
782
+ get currentPath() {
783
+ return state.currentPath;
784
+ },
785
+
786
+ setCwd(cwd) {
787
+ state.cwd = cwd;
788
+ },
789
+
790
+ resetSize() {
791
+ resetSize();
792
+ if (widget && WidgetManager.isOpen('file-preview')) {
793
+ debug.log('[FilePreviewWidget] Size reset to defaults:', DEFAULT_WIDTH, 'x', DEFAULT_HEIGHT);
794
+ }
795
+ },
796
+
797
+ getSizeSettings() {
798
+ const saved = loadSavedSize();
799
+ return {
800
+ width: saved.width,
801
+ height: saved.height,
802
+ defaultWidth: DEFAULT_WIDTH,
803
+ defaultHeight: DEFAULT_HEIGHT,
804
+ isCustom: saved.width !== DEFAULT_WIDTH || saved.height !== DEFAULT_HEIGHT
805
+ };
806
+ },
807
+
808
+ openSearch() {
809
+ openSearch();
810
+ },
811
+
812
+ closeSearch() {
813
+ closeSearch();
814
+ },
815
+
816
+ isSearchActive() {
817
+ return state.search.active;
818
+ },
819
+
820
+ closeEditorSearch() {
821
+ if (state.editor?.isSearchOpen?.()) {
822
+ state.editor.closeSearch();
823
+ return true;
824
+ }
825
+ return false;
826
+ },
827
+
828
+ findNext() {
829
+ navigateSearch(1);
830
+ },
831
+
832
+ findPrevious() {
833
+ navigateSearch(-1);
834
+ },
835
+
836
+ async switchToEditView() {
837
+ return switchToEditView();
838
+ },
839
+
840
+ leaveEditView(targetMode = 'code') {
841
+ return leaveEditView(targetMode);
842
+ },
843
+
844
+ discardEdits() {
845
+ return discardEdits();
846
+ },
847
+
848
+ async saveFile() {
849
+ return saveFile();
850
+ },
851
+
852
+ get isEditing() {
853
+ return isEditMode();
854
+ },
855
+
856
+ get isModified() {
857
+ return state.modified;
858
+ },
859
+
860
+ removeTabState,
861
+ };