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,486 @@
1
+ /**
2
+ * Smart Diff Utilities
3
+ *
4
+ * Provides human-friendly diff visualization:
5
+ * - Line matching (don't duplicate unchanged lines)
6
+ * - Word-level highlighting for modified lines
7
+ * - Collapsible unchanged sections
8
+ */
9
+
10
+ /**
11
+ * Compute the longest common subsequence of two arrays
12
+ * Returns indices mapping: [oldIndex, newIndex] pairs for matching lines
13
+ */
14
+ function computeLCS(oldLines, newLines) {
15
+ const m = oldLines.length;
16
+ const n = newLines.length;
17
+
18
+ // DP table
19
+ const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
20
+
21
+ for (let i = 1; i <= m; i++) {
22
+ for (let j = 1; j <= n; j++) {
23
+ if (oldLines[i - 1] === newLines[j - 1]) {
24
+ dp[i][j] = dp[i - 1][j - 1] + 1;
25
+ } else {
26
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
27
+ }
28
+ }
29
+ }
30
+
31
+ // Backtrack to find the actual LCS
32
+ const matches = [];
33
+ let i = m, j = n;
34
+ while (i > 0 && j > 0) {
35
+ if (oldLines[i - 1] === newLines[j - 1]) {
36
+ matches.unshift([i - 1, j - 1]);
37
+ i--; j--;
38
+ } else if (dp[i - 1][j] > dp[i][j - 1]) {
39
+ i--;
40
+ } else {
41
+ j--;
42
+ }
43
+ }
44
+
45
+ return matches;
46
+ }
47
+
48
+ /**
49
+ * Find word-level differences between two similar lines
50
+ * Returns HTML with highlighted changes
51
+ */
52
+ function computeWordDiff(oldLine, newLine, escapeHtml) {
53
+ // Tokenize by word boundaries (keep whitespace as tokens)
54
+ const tokenize = (str) => str.match(/\S+|\s+/g) || [''];
55
+
56
+ const oldTokens = tokenize(oldLine);
57
+ const newTokens = tokenize(newLine);
58
+
59
+ // Find common prefix
60
+ let prefixLen = 0;
61
+ while (prefixLen < oldTokens.length &&
62
+ prefixLen < newTokens.length &&
63
+ oldTokens[prefixLen] === newTokens[prefixLen]) {
64
+ prefixLen++;
65
+ }
66
+
67
+ // Find common suffix (from the remaining tokens)
68
+ let oldSuffixStart = oldTokens.length;
69
+ let newSuffixStart = newTokens.length;
70
+ while (oldSuffixStart > prefixLen &&
71
+ newSuffixStart > prefixLen &&
72
+ oldTokens[oldSuffixStart - 1] === newTokens[newSuffixStart - 1]) {
73
+ oldSuffixStart--;
74
+ newSuffixStart--;
75
+ }
76
+
77
+ // Build HTML for old line (removed parts highlighted)
78
+ // Skip wrapping whitespace-only tokens to avoid cluttered highlight boxes
79
+ const buildHtml = (tokens, start, end, highlightClass) => {
80
+ let html = '';
81
+ for (let i = 0; i < tokens.length; i++) {
82
+ const token = escapeHtml(tokens[i]);
83
+ const isWhitespace = /^\s+$/.test(tokens[i]);
84
+ if (i >= start && i < end && !isWhitespace) {
85
+ html += `<span class="${highlightClass}">${token}</span>`;
86
+ } else {
87
+ html += token;
88
+ }
89
+ }
90
+ return html;
91
+ };
92
+
93
+ return {
94
+ oldHtml: buildHtml(oldTokens, prefixLen, oldSuffixStart, 'diff-word-removed'),
95
+ newHtml: buildHtml(newTokens, prefixLen, newSuffixStart, 'diff-word-added')
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Check if two lines are "similar enough" to show as modified
101
+ * rather than as separate remove + add
102
+ */
103
+ function areSimilarLines(oldLine, newLine) {
104
+ if (!oldLine || !newLine) return false;
105
+
106
+ // If one is much longer than the other, not similar
107
+ const lenRatio = Math.min(oldLine.length, newLine.length) /
108
+ Math.max(oldLine.length, newLine.length);
109
+ if (lenRatio < 0.3) return false;
110
+
111
+ // Check common prefix/suffix ratio
112
+ let commonChars = 0;
113
+ const minLen = Math.min(oldLine.length, newLine.length);
114
+
115
+ // Count matching prefix
116
+ for (let i = 0; i < minLen && oldLine[i] === newLine[i]; i++) {
117
+ commonChars++;
118
+ }
119
+
120
+ // Count matching suffix
121
+ for (let i = 0; i < minLen &&
122
+ oldLine[oldLine.length - 1 - i] === newLine[newLine.length - 1 - i] &&
123
+ commonChars < minLen; i++) {
124
+ commonChars++;
125
+ }
126
+
127
+ // Consider similar if >40% characters match in prefix+suffix
128
+ return commonChars / Math.max(oldLine.length, newLine.length) > 0.4;
129
+ }
130
+
131
+ /**
132
+ * Generate a smart unified diff
133
+ *
134
+ * @param {string[]} oldLines - Original lines
135
+ * @param {string[]} newLines - New lines
136
+ * @param {number} startLine - Starting line number in file
137
+ * @param {function} escapeHtml - HTML escaping function
138
+ * @param {object} options - Options for diff generation
139
+ * @returns {object[]} Array of diff entries
140
+ */
141
+ function generateSmartDiff(oldLines, newLines, startLine, escapeHtml, options = {}) {
142
+ const { contextLines = 2, collapseThreshold = 4 } = options;
143
+
144
+ const matches = computeLCS(oldLines, newLines);
145
+ const result = [];
146
+
147
+ let oldIdx = 0;
148
+ let newIdx = 0;
149
+ let matchIdx = 0;
150
+
151
+ // Track line numbers (separate for old and new)
152
+ let oldLineNum = startLine;
153
+ let newLineNum = startLine;
154
+
155
+ while (oldIdx < oldLines.length || newIdx < newLines.length) {
156
+ // Check if current position is a match
157
+ if (matchIdx < matches.length &&
158
+ matches[matchIdx][0] === oldIdx &&
159
+ matches[matchIdx][1] === newIdx) {
160
+ // This is an unchanged line
161
+ result.push({
162
+ type: 'context',
163
+ oldLineNum: oldLineNum,
164
+ newLineNum: newLineNum,
165
+ content: escapeHtml(oldLines[oldIdx]) || ' '
166
+ });
167
+ oldIdx++; newIdx++; matchIdx++;
168
+ oldLineNum++; newLineNum++;
169
+ } else {
170
+ // Collect consecutive changes until next match
171
+ const nextMatch = matchIdx < matches.length ? matches[matchIdx] : [oldLines.length, newLines.length];
172
+
173
+ const removedLines = [];
174
+ const addedLines = [];
175
+
176
+ while (oldIdx < nextMatch[0]) {
177
+ removedLines.push({ idx: oldIdx, line: oldLines[oldIdx], lineNum: oldLineNum });
178
+ oldIdx++; oldLineNum++;
179
+ }
180
+ while (newIdx < nextMatch[1]) {
181
+ addedLines.push({ idx: newIdx, line: newLines[newIdx], lineNum: newLineNum });
182
+ newIdx++; newLineNum++;
183
+ }
184
+
185
+ // Try to pair similar lines for inline diff
186
+ const paired = [];
187
+ const unpaired = { removed: [...removedLines], added: [...addedLines] };
188
+
189
+ // Simple greedy pairing of similar lines
190
+ for (let r = 0; r < unpaired.removed.length; r++) {
191
+ for (let a = 0; a < unpaired.added.length; a++) {
192
+ if (areSimilarLines(unpaired.removed[r].line, unpaired.added[a].line)) {
193
+ paired.push({
194
+ removed: unpaired.removed[r],
195
+ added: unpaired.added[a]
196
+ });
197
+ unpaired.removed.splice(r, 1);
198
+ unpaired.added.splice(a, 1);
199
+ r--; // Adjust index after splice
200
+ break;
201
+ }
202
+ }
203
+ }
204
+
205
+ // Collect all change entries with sort keys
206
+ const changeEntries = [];
207
+
208
+ // Unpaired removed lines (use old line num as sort key)
209
+ for (const r of unpaired.removed) {
210
+ changeEntries.push({
211
+ type: 'removed',
212
+ oldLineNum: r.lineNum,
213
+ sortKey: r.lineNum, // Sort by old position
214
+ content: escapeHtml(r.line) || ' '
215
+ });
216
+ }
217
+
218
+ // Paired (modified) lines with word diff
219
+ for (const p of paired) {
220
+ const wordDiff = computeWordDiff(p.removed.line, p.added.line, escapeHtml);
221
+ changeEntries.push({
222
+ type: 'modified',
223
+ oldLineNum: p.removed.lineNum,
224
+ newLineNum: p.added.lineNum,
225
+ sortKey: p.added.lineNum, // Sort by new position
226
+ oldContent: wordDiff.oldHtml,
227
+ newContent: wordDiff.newHtml
228
+ });
229
+ }
230
+
231
+ // Unpaired added lines
232
+ for (const a of unpaired.added) {
233
+ changeEntries.push({
234
+ type: 'added',
235
+ newLineNum: a.lineNum,
236
+ sortKey: a.lineNum, // Sort by new position
237
+ content: escapeHtml(a.line) || ' '
238
+ });
239
+ }
240
+
241
+ // Sort all changes by their position and add to result
242
+ changeEntries.sort((a, b) => a.sortKey - b.sortKey);
243
+ for (const entry of changeEntries) {
244
+ delete entry.sortKey; // Remove temporary sort key
245
+ result.push(entry);
246
+ }
247
+ }
248
+ }
249
+
250
+ // Collapse long sequences of context lines
251
+ return collapseContext(result, contextLines, collapseThreshold);
252
+ }
253
+
254
+ /**
255
+ * Collapse long sequences of unchanged context lines
256
+ */
257
+ function collapseContext(entries, contextLines, threshold) {
258
+ const result = [];
259
+ let contextRun = [];
260
+
261
+ for (const entry of entries) {
262
+ if (entry.type === 'context') {
263
+ contextRun.push(entry);
264
+ } else {
265
+ // Flush context run
266
+ if (contextRun.length > 0) {
267
+ result.push(...collapseRun(contextRun, contextLines, threshold));
268
+ contextRun = [];
269
+ }
270
+ result.push(entry);
271
+ }
272
+ }
273
+
274
+ // Flush final context run
275
+ if (contextRun.length > 0) {
276
+ result.push(...collapseRun(contextRun, contextLines, threshold));
277
+ }
278
+
279
+ return result;
280
+ }
281
+
282
+ /**
283
+ * Collapse a run of context lines, keeping contextLines at start/end
284
+ */
285
+ function collapseRun(run, contextLines, threshold) {
286
+ // Check if this run is at the start or end (no changes around it)
287
+ // For simplicity, if it's a long run, collapse the middle
288
+
289
+ if (run.length <= threshold) {
290
+ return run;
291
+ }
292
+
293
+ const result = [];
294
+
295
+ // Keep first contextLines
296
+ for (let i = 0; i < Math.min(contextLines, run.length); i++) {
297
+ result.push(run[i]);
298
+ }
299
+
300
+ // Add collapse marker
301
+ const collapsedCount = run.length - contextLines * 2;
302
+ if (collapsedCount > 0) {
303
+ result.push({
304
+ type: 'collapse',
305
+ count: collapsedCount,
306
+ startLine: run[contextLines].oldLineNum || run[contextLines].newLineNum,
307
+ endLine: run[run.length - contextLines - 1].oldLineNum || run[run.length - contextLines - 1].newLineNum
308
+ });
309
+ }
310
+
311
+ // Keep last contextLines
312
+ for (let i = Math.max(contextLines, run.length - contextLines); i < run.length; i++) {
313
+ result.push(run[i]);
314
+ }
315
+
316
+ return result;
317
+ }
318
+
319
+ /**
320
+ * Render a single diff entry as a row (no wrapper).
321
+ * `hidden` rows get .diff-row-hidden — shown only when the block is expanded.
322
+ */
323
+ function renderDiffRow(entry, hidden = false) {
324
+ const h = hidden ? ' diff-row-hidden' : '';
325
+ switch (entry.type) {
326
+ case 'context':
327
+ return `<div class="diff-line diff-context${h}">
328
+ <span class="diff-line-num">${entry.oldLineNum}</span>
329
+ <span class="diff-prefix"> </span>
330
+ <span class="diff-text">${entry.content}</span>
331
+ </div>`;
332
+ case 'removed':
333
+ return `<div class="diff-line diff-removed${h}">
334
+ <span class="diff-line-num">${entry.oldLineNum}</span>
335
+ <span class="diff-prefix">−</span>
336
+ <span class="diff-text">${entry.content}</span>
337
+ </div>`;
338
+ case 'added':
339
+ return `<div class="diff-line diff-added${h}">
340
+ <span class="diff-line-num">${entry.newLineNum}</span>
341
+ <span class="diff-prefix">+</span>
342
+ <span class="diff-text">${entry.content}</span>
343
+ </div>`;
344
+ case 'modified':
345
+ return `<div class="diff-line diff-removed${h}">
346
+ <span class="diff-line-num">${entry.oldLineNum}</span>
347
+ <span class="diff-prefix">−</span>
348
+ <span class="diff-text">${entry.oldContent}</span>
349
+ </div><div class="diff-line diff-added${h}">
350
+ <span class="diff-line-num">${entry.newLineNum}</span>
351
+ <span class="diff-prefix">+</span>
352
+ <span class="diff-text">${entry.newContent}</span>
353
+ </div>`;
354
+ case 'collapse':
355
+ return `<div class="diff-line diff-collapse${h}" data-start="${entry.startLine}" data-end="${entry.endLine}">
356
+ <span class="diff-line-num">⋯</span>
357
+ <span class="diff-prefix"></span>
358
+ <span class="diff-collapse-text">${entry.count} unchanged lines</span>
359
+ </div>`;
360
+ default:
361
+ return '';
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Render smart diff to HTML.
367
+ *
368
+ * Rows are grouped into hunks so each hunk shares one horizontal scrollbar:
369
+ * - All rows between collapse separators (context and changes alike) → one hunk.
370
+ * - Collapse rows render bare (they have their own click affordance and must
371
+ * stay full-width, so they can't live inside a scrolling hunk).
372
+ *
373
+ * options.hideAfter: rows past this count get .diff-row-hidden (shown when the
374
+ * enclosing block gains .expanded) — used to keep tall diffs short by default.
375
+ */
376
+ function renderSmartDiff(entries, options = {}) {
377
+ const hideAfter = options.hideAfter ?? Infinity;
378
+ let html = '';
379
+ let buf = '';
380
+ let rowCount = 0;
381
+
382
+ const flush = () => {
383
+ if (buf) {
384
+ html += `<div class="diff-hunk"><div class="diff-hunk-inner">${buf}</div></div>`;
385
+ buf = '';
386
+ }
387
+ };
388
+
389
+ for (const entry of entries) {
390
+ const hidden = rowCount >= hideAfter;
391
+ rowCount += entry.type === 'modified' ? 2 : 1;
392
+ if (entry.type === 'collapse') {
393
+ flush();
394
+ html += renderDiffRow(entry, hidden);
395
+ continue;
396
+ }
397
+ buf += renderDiffRow(entry, hidden);
398
+ }
399
+ flush();
400
+
401
+ return html;
402
+ }
403
+
404
+ /**
405
+ * Render a single diff entry as a row of side-by-side grid cells.
406
+ */
407
+ function renderSbsRow(entry, hunkIdx) {
408
+ const hunkAttr = entry.type !== 'context' ? ` data-hunk="${hunkIdx}"` : '';
409
+ switch (entry.type) {
410
+ case 'context':
411
+ return `<div class="sbs-linenum sbs-left sbs-context">${entry.oldLineNum}</div>`
412
+ + `<div class="sbs-text sbs-left sbs-context">${entry.content}</div>`
413
+ + `<div class="sbs-gutter"></div>`
414
+ + `<div class="sbs-linenum sbs-right sbs-context">${entry.newLineNum}</div>`
415
+ + `<div class="sbs-text sbs-right sbs-context">${entry.content}</div>`;
416
+ case 'removed':
417
+ return `<div class="sbs-linenum sbs-left sbs-removed"${hunkAttr}>${entry.oldLineNum}</div>`
418
+ + `<div class="sbs-text sbs-left sbs-removed">${entry.content}</div>`
419
+ + `<div class="sbs-gutter"></div>`
420
+ + `<div class="sbs-linenum sbs-right sbs-empty"></div>`
421
+ + `<div class="sbs-text sbs-right sbs-empty"></div>`;
422
+ case 'added':
423
+ return `<div class="sbs-linenum sbs-left sbs-empty"></div>`
424
+ + `<div class="sbs-text sbs-left sbs-empty"></div>`
425
+ + `<div class="sbs-gutter"></div>`
426
+ + `<div class="sbs-linenum sbs-right sbs-added"${hunkAttr}>${entry.newLineNum}</div>`
427
+ + `<div class="sbs-text sbs-right sbs-added">${entry.content}</div>`;
428
+ case 'modified':
429
+ return `<div class="sbs-linenum sbs-left sbs-modified-old"${hunkAttr}>${entry.oldLineNum}</div>`
430
+ + `<div class="sbs-text sbs-left sbs-modified-old">${entry.oldContent}</div>`
431
+ + `<div class="sbs-gutter"></div>`
432
+ + `<div class="sbs-linenum sbs-right sbs-modified-new">${entry.newLineNum}</div>`
433
+ + `<div class="sbs-text sbs-right sbs-modified-new">${entry.newContent}</div>`;
434
+ default:
435
+ return '';
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Render smart diff as a stack of side-by-side hunks.
441
+ *
442
+ * All rows between collapse markers (context and changes alike) share one
443
+ * scroll-container grid, so the whole section scrolls with one horizontal
444
+ * scrollbar. data-hunk indices still advance per change group (not per
445
+ * scroll container) so keyboard hunk navigation is unaffected.
446
+ *
447
+ * @param {object[]} entries - diff entries from generateSmartDiff
448
+ * @param {object} options
449
+ * @param {function} options.collapseLabel - (count) => string for the "N unchanged lines" marker
450
+ */
451
+ function renderSideBySideDiff(entries, options = {}) {
452
+ const collapseLabel = options.collapseLabel || ((count) => `${count} unchanged lines`);
453
+ let html = '<div class="sbs-diff-stack">';
454
+ let buf = '';
455
+ let prevType = null;
456
+ let hunkIdx = 0;
457
+
458
+ const flush = () => {
459
+ if (buf) {
460
+ html += `<div class="sbs-hunk"><div class="sbs-diff-grid">${buf}</div></div>`;
461
+ buf = '';
462
+ }
463
+ };
464
+
465
+ for (const entry of entries) {
466
+ if (entry.type === 'collapse') {
467
+ flush();
468
+ html += `<div class="sbs-collapse" data-start="${entry.startLine}" data-end="${entry.endLine}">`
469
+ + `<span>⋯</span> ${collapseLabel(entry.count)}</div>`;
470
+ hunkIdx++;
471
+ prevType = null;
472
+ continue;
473
+ }
474
+
475
+ const thisType = entry.type === 'context' ? 'context' : 'change';
476
+ if (prevType === 'change' && thisType !== 'change') hunkIdx++;
477
+ prevType = thisType;
478
+ buf += renderSbsRow(entry, hunkIdx);
479
+ }
480
+ flush();
481
+
482
+ html += '</div>';
483
+ return html;
484
+ }
485
+
486
+ export { generateSmartDiff, renderSmartDiff, renderSideBySideDiff, computeWordDiff, areSimilarLines };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Drafts Pill
3
+ *
4
+ * Small clickable "N drafts" pill in the top-right corner of the empty chat
5
+ * input — the visible entry point to Prompt Explorer → Drafts. Drafts are
6
+ * auto-banked silently while typing (see InputHandler's draft auto-sync),
7
+ * so without this pill they'd be invisible until the user opened the
8
+ * Prompt Explorer by hand.
9
+ *
10
+ * Visibility mirrors the shortcut-hints overlay lifecycle: shown only when
11
+ * the input is empty, not in shell/plan mode, and at least one draft exists.
12
+ * Count refreshes on `drafts-changed` window events (dispatched by every
13
+ * draft mutation path) and lazily when the input empties.
14
+ */
15
+
16
+ import S from './strings.js';
17
+ import { CONFIG } from './config.js';
18
+ import { openPromptExplorerDrafts } from './widgets/prompt-explorer-widget.js';
19
+
20
+ // Don't refetch the count more often than this on input-emptied transitions
21
+ // (drafts-changed events always refetch — they signal a real mutation)
22
+ const REFRESH_THROTTLE_MS = 60000;
23
+
24
+ class DraftsPillController {
25
+ constructor() {
26
+ this.el = null;
27
+ this.count = 0;
28
+ this.inputEmpty = true;
29
+ this.suppressed = false; // forced off by mode (shell/plan)
30
+ this._lastFetch = 0;
31
+ }
32
+
33
+ init() {
34
+ this.el = document.getElementById('drafts-pill');
35
+ if (!this.el) return;
36
+ this.el.setAttribute('data-tooltip', S.ui.input.drafts_pill_tooltip);
37
+ this.el.addEventListener('click', () => openPromptExplorerDrafts());
38
+ window.addEventListener('drafts-changed', () => this.refresh());
39
+ this.refresh();
40
+ }
41
+
42
+ /** Refetch the draft count from the server. */
43
+ async refresh() {
44
+ this._lastFetch = Date.now();
45
+ try {
46
+ const res = await fetch(`${CONFIG.API_BASE}/api/drafts`);
47
+ if (!res.ok) return;
48
+ const data = await res.json();
49
+ this.count = data.count ?? (data.drafts || []).length;
50
+ } catch {
51
+ /* offline/auth hiccup — keep last known count */
52
+ }
53
+ this._sync();
54
+ }
55
+
56
+ /** Called by input-handler whenever input content changes. */
57
+ updateVisibility(textareaValue) {
58
+ const isEmpty = !textareaValue || textareaValue.length === 0;
59
+ const becameEmpty = isEmpty && !this.inputEmpty;
60
+ this.inputEmpty = isEmpty;
61
+ this._sync();
62
+ // Input just emptied → opportunistic freshness check (throttled)
63
+ if (becameEmpty && Date.now() - this._lastFetch > REFRESH_THROTTLE_MS) {
64
+ this.refresh();
65
+ }
66
+ }
67
+
68
+ /** Called by input-handler whenever input mode changes. */
69
+ setMode(mode) {
70
+ this.suppressed = mode === 'shell' || mode === 'plan';
71
+ this._sync();
72
+ }
73
+
74
+ _sync() {
75
+ if (!this.el) return;
76
+ const show = this.count > 0 && this.inputEmpty && !this.suppressed;
77
+ if (show) {
78
+ this.el.textContent = this.count === 1
79
+ ? S.ui.input.drafts_pill_one
80
+ : S.ui.input.drafts_pill_many.replace('{count}', this.count);
81
+ }
82
+ this.el.classList.toggle('visible', show);
83
+ }
84
+ }
85
+
86
+ export const DraftsPill = new DraftsPillController();
87
+
88
+ // Initialize on DOM ready (same pattern as ShortcutHints)
89
+ if (typeof document !== 'undefined') {
90
+ if (document.readyState === 'loading') {
91
+ document.addEventListener('DOMContentLoaded', () => DraftsPill.init());
92
+ } else {
93
+ DraftsPill.init();
94
+ }
95
+ }