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,738 @@
1
+ /**
2
+ * Snippets Autocomplete Module
3
+ * Provides suggestions for agents and user-defined text snippets when typing # in chat input
4
+ *
5
+ * Data is stored server-side in ~/.painapple-code/config.json
6
+ * with localStorage as cache/fallback for offline use.
7
+ */
8
+
9
+ import S from './strings.js';
10
+ import { escapeHtml } from './utils.js';
11
+ import { CONFIG } from './config.js';
12
+ import { anchorAbove } from './caret-position.js';
13
+
14
+ /**
15
+ * Get user-facing agents — discovered from ~/.claude/agents/ and the
16
+ * project's `<cwd>/.claude/agents/`. Claude's hardcoded built-ins
17
+ * (Explore, Plan, general-purpose) are intentionally excluded — they
18
+ * are agents Claude uses internally, not ones humans typically invoke
19
+ * via `#`.
20
+ */
21
+ export function getBuiltInAgents() {
22
+ return loadDiscoveredAgents();
23
+ }
24
+
25
+ // localStorage cache keys (fallback for offline use)
26
+ const SNIPPETS_CACHE_KEY = 'claude-code-snippets-cache';
27
+ const LEGACY_SNIPPETS_CACHE_KEY = 'claude-code-favorites-cache';
28
+ const DISABLED_AGENTS_CACHE_KEY = 'claude-code-disabled-agents-cache';
29
+ const AGENT_PATTERNS_CACHE_KEY = 'claude-code-agent-patterns-cache';
30
+ const DISCOVERED_AGENTS_CACHE_KEY = 'claude-code-discovered-agents-cache';
31
+
32
+ // Default pattern - {agent} is replaced with agent name
33
+ export const DEFAULT_AGENT_PATTERN = S.prompts.agents.default_pattern;
34
+
35
+ // In-memory cache (populated from server or localStorage)
36
+ let _snippetsCache = null;
37
+ let _disabledAgentsCache = null;
38
+ let _agentPatternsCache = null;
39
+ let _discoveredAgentsCache = null;
40
+
41
+ // ═══════════════════════════════════════════════════════════════════════════
42
+ // Server API Functions
43
+ // ═══════════════════════════════════════════════════════════════════════════
44
+
45
+ /**
46
+ * Fetch user snippets from server
47
+ */
48
+ export async function fetchUserSnippets() {
49
+ try {
50
+ const response = await fetch(`${CONFIG.API_BASE}/api/user/snippets`);
51
+ if (response.ok) {
52
+ const data = await response.json();
53
+ _snippetsCache = data.snippets || [];
54
+ _disabledAgentsCache = new Set(data.disabled_agents || []);
55
+ localStorage.setItem(SNIPPETS_CACHE_KEY, JSON.stringify(_snippetsCache));
56
+ localStorage.setItem(DISABLED_AGENTS_CACHE_KEY, JSON.stringify([..._disabledAgentsCache]));
57
+ localStorage.removeItem(LEGACY_SNIPPETS_CACHE_KEY);
58
+ return data;
59
+ }
60
+ } catch (e) {
61
+ console.error('Failed to fetch snippets from server:', e);
62
+ }
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * Save user snippets to server
68
+ */
69
+ export async function saveUserSnippets(snippets, disabledAgents) {
70
+ _snippetsCache = snippets;
71
+ _disabledAgentsCache = disabledAgents instanceof Set ? disabledAgents : new Set(disabledAgents);
72
+ localStorage.setItem(SNIPPETS_CACHE_KEY, JSON.stringify(_snippetsCache));
73
+ localStorage.setItem(DISABLED_AGENTS_CACHE_KEY, JSON.stringify([..._disabledAgentsCache]));
74
+
75
+ try {
76
+ const response = await fetch(`${CONFIG.API_BASE}/api/user/snippets`, {
77
+ method: 'PUT',
78
+ headers: { 'Content-Type': 'application/json' },
79
+ body: JSON.stringify({
80
+ snippets: snippets,
81
+ disabled_agents: [..._disabledAgentsCache]
82
+ })
83
+ });
84
+ return response.ok;
85
+ } catch (e) {
86
+ console.error('Failed to save snippets to server:', e);
87
+ return false;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Fetch agent patterns from server
93
+ */
94
+ export async function fetchAgentPatterns() {
95
+ try {
96
+ const response = await fetch(`${CONFIG.API_BASE}/api/user/agent-patterns`);
97
+ if (response.ok) {
98
+ const data = await response.json();
99
+ _agentPatternsCache = data;
100
+ localStorage.setItem(AGENT_PATTERNS_CACHE_KEY, JSON.stringify(data));
101
+ return data;
102
+ }
103
+ } catch (e) {
104
+ console.error('Failed to fetch agent patterns from server:', e);
105
+ }
106
+ return null;
107
+ }
108
+
109
+ /**
110
+ * Save agent patterns to server
111
+ */
112
+ export async function saveAgentPatternsToServer(patterns) {
113
+ _agentPatternsCache = patterns;
114
+ localStorage.setItem(AGENT_PATTERNS_CACHE_KEY, JSON.stringify(patterns));
115
+
116
+ try {
117
+ const response = await fetch(`${CONFIG.API_BASE}/api/user/agent-patterns`, {
118
+ method: 'PUT',
119
+ headers: { 'Content-Type': 'application/json' },
120
+ body: JSON.stringify(patterns)
121
+ });
122
+ return response.ok;
123
+ } catch (e) {
124
+ console.error('Failed to save agent patterns to server:', e);
125
+ return false;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Fetch discovered agents from ~/.claude/agents/ and optionally {cwd}/.claude/agents/
131
+ * These are user-defined agents with YAML frontmatter
132
+ * @param {string} [cwd] - Optional project directory to scan for project-local agents
133
+ */
134
+ export async function fetchDiscoveredAgents(cwd = null) {
135
+ try {
136
+ let url = `${CONFIG.API_BASE}/api/agents`;
137
+ if (cwd) {
138
+ url += `?cwd=${encodeURIComponent(cwd)}`;
139
+ }
140
+ const response = await fetch(url);
141
+ if (response.ok) {
142
+ const data = await response.json();
143
+ const agents = (data.agents || []).map(agent => ({
144
+ id: agent.id,
145
+ name: agent.name,
146
+ desc: agent.description || '',
147
+ type: 'agent',
148
+ file: agent.file,
149
+ source: agent.source || 'global'
150
+ }));
151
+ _discoveredAgentsCache = agents;
152
+ localStorage.setItem(DISCOVERED_AGENTS_CACHE_KEY, JSON.stringify(agents));
153
+ return agents;
154
+ }
155
+ } catch (e) {
156
+ console.error('Failed to fetch discovered agents:', e);
157
+ }
158
+ return null;
159
+ }
160
+
161
+ // ═══════════════════════════════════════════════════════════════════════════
162
+ // Synchronous Access Functions (use cache/localStorage)
163
+ // ═══════════════════════════════════════════════════════════════════════════
164
+
165
+ /**
166
+ * Load disabled agents set (from cache or localStorage)
167
+ */
168
+ export function loadDisabledAgents() {
169
+ if (_disabledAgentsCache) {
170
+ return _disabledAgentsCache;
171
+ }
172
+ try {
173
+ const saved = localStorage.getItem(DISABLED_AGENTS_CACHE_KEY);
174
+ if (saved) {
175
+ _disabledAgentsCache = new Set(JSON.parse(saved));
176
+ return _disabledAgentsCache;
177
+ }
178
+ } catch (e) {
179
+ console.error('Failed to load disabled agents:', e);
180
+ }
181
+ return new Set();
182
+ }
183
+
184
+ /**
185
+ * Save disabled agents set (updates cache and server)
186
+ */
187
+ export function saveDisabledAgents(disabledSet) {
188
+ _disabledAgentsCache = disabledSet;
189
+ localStorage.setItem(DISABLED_AGENTS_CACHE_KEY, JSON.stringify([...disabledSet]));
190
+ saveUserSnippets(loadSnippets(), disabledSet);
191
+ }
192
+
193
+ /**
194
+ * Load user snippets (from cache or localStorage)
195
+ */
196
+ export function loadSnippets() {
197
+ if (_snippetsCache) {
198
+ return _snippetsCache;
199
+ }
200
+ try {
201
+ const saved = localStorage.getItem(SNIPPETS_CACHE_KEY)
202
+ ?? localStorage.getItem(LEGACY_SNIPPETS_CACHE_KEY);
203
+ if (saved) {
204
+ _snippetsCache = JSON.parse(saved);
205
+ return _snippetsCache;
206
+ }
207
+ } catch (e) {
208
+ console.error('Failed to load snippets:', e);
209
+ }
210
+ return [];
211
+ }
212
+
213
+ /**
214
+ * Save user snippets (updates cache and server)
215
+ */
216
+ export function saveSnippets(snippets) {
217
+ _snippetsCache = snippets;
218
+ localStorage.setItem(SNIPPETS_CACHE_KEY, JSON.stringify(snippets));
219
+ saveUserSnippets(snippets, loadDisabledAgents());
220
+ }
221
+
222
+ /**
223
+ * Load discovered agents (from cache or localStorage)
224
+ * These are agents from ~/.claude/agents/
225
+ */
226
+ export function loadDiscoveredAgents() {
227
+ if (_discoveredAgentsCache) {
228
+ return _discoveredAgentsCache;
229
+ }
230
+ try {
231
+ const saved = localStorage.getItem(DISCOVERED_AGENTS_CACHE_KEY);
232
+ if (saved) {
233
+ _discoveredAgentsCache = JSON.parse(saved);
234
+ return _discoveredAgentsCache;
235
+ }
236
+ } catch (e) {
237
+ console.error('Failed to load discovered agents:', e);
238
+ }
239
+ return [];
240
+ }
241
+
242
+ /**
243
+ * Load agent patterns (from cache or localStorage)
244
+ */
245
+ export function loadAgentPatterns() {
246
+ if (_agentPatternsCache) {
247
+ return _agentPatternsCache;
248
+ }
249
+ try {
250
+ const saved = localStorage.getItem(AGENT_PATTERNS_CACHE_KEY);
251
+ if (saved) {
252
+ _agentPatternsCache = JSON.parse(saved);
253
+ return _agentPatternsCache;
254
+ }
255
+ } catch (e) {
256
+ console.error('Failed to load agent patterns:', e);
257
+ }
258
+ return { global: DEFAULT_AGENT_PATTERN, agents: {} };
259
+ }
260
+
261
+ /**
262
+ * Save agent patterns (updates cache and server)
263
+ */
264
+ export function saveAgentPatterns(patterns) {
265
+ _agentPatternsCache = patterns;
266
+ localStorage.setItem(AGENT_PATTERNS_CACHE_KEY, JSON.stringify(patterns));
267
+ saveAgentPatternsToServer(patterns);
268
+ }
269
+
270
+ /**
271
+ * Get the pattern to use for a specific agent
272
+ */
273
+ export function getAgentPattern(agentId) {
274
+ const patterns = loadAgentPatterns();
275
+ return patterns.agents[agentId] || patterns.global || DEFAULT_AGENT_PATTERN;
276
+ }
277
+
278
+ /**
279
+ * Initialize snippets data from server
280
+ * Call this on app startup
281
+ * @param {string} [cwd] - Optional project directory for project-local agents
282
+ */
283
+ export async function initSnippetsData(cwd = null) {
284
+ await Promise.all([
285
+ fetchUserSnippets(),
286
+ fetchAgentPatterns(),
287
+ fetchDiscoveredAgents(cwd)
288
+ ]);
289
+ }
290
+
291
+ /**
292
+ * Refresh agents for a new cwd (call when session cwd changes)
293
+ * @param {string} cwd - The new project directory
294
+ */
295
+ export async function refreshAgentsForCwd(cwd) {
296
+ return fetchDiscoveredAgents(cwd);
297
+ }
298
+
299
+ /**
300
+ * SnippetsAutocomplete - Suggestions for # trigger
301
+ */
302
+ export class SnippetsAutocomplete {
303
+ /**
304
+ * @param {HTMLTextAreaElement} input - The message input element
305
+ * @param {Object} options - Configuration options
306
+ */
307
+ constructor(input, options = {}) {
308
+ this.input = input;
309
+
310
+ this.visible = false;
311
+ this.items = [];
312
+ this.selectedIndex = 0;
313
+ this.triggerPos = -1;
314
+ this.query = '';
315
+
316
+ this.container = document.createElement('div');
317
+ this.container.id = 'snippets-autocomplete';
318
+ this.container.className = 'snippets-autocomplete';
319
+
320
+ this.input.parentElement.insertBefore(this.container, this.input);
321
+
322
+ this._handleClick = this._handleClick.bind(this);
323
+ this.container.addEventListener('click', this._handleClick);
324
+
325
+ this._handleModifier = this._handleModifier.bind(this);
326
+ }
327
+
328
+ /**
329
+ * Track Alt / Ctrl-Cmd modifier state while the dropdown is visible
330
+ * so the highlighted action icon reflects what Enter will do.
331
+ *
332
+ * Also fires the corresponding action on Enter+modifier here in capture
333
+ * phase, so the action can't be swallowed by an upstream global keydown
334
+ * listener (this kept biting on iPad and Mac where Cmd+Enter goes
335
+ * through document-level shortcut dispatchers).
336
+ */
337
+ _handleModifier(e) {
338
+ let mod = '';
339
+ if (e.ctrlKey || e.metaKey) mod = 'mod-ctrl';
340
+ else if (e.altKey) mod = 'mod-alt';
341
+ if (mod !== this._currentMod) {
342
+ this.container.classList.remove('mod-alt', 'mod-ctrl');
343
+ if (mod) this.container.classList.add(mod);
344
+ this._currentMod = mod;
345
+ }
346
+
347
+ if (e.type === 'keydown' && e.key === 'Enter' && this.hasSelection()) {
348
+ if (e.ctrlKey || e.metaKey) {
349
+ e.preventDefault();
350
+ e.stopImmediatePropagation();
351
+ this.selectAndSend();
352
+ return;
353
+ }
354
+ if (e.altKey) {
355
+ e.preventDefault();
356
+ e.stopImmediatePropagation();
357
+ this.select(undefined, { short: true });
358
+ return;
359
+ }
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Get all items (discovered agents + user snippets)
365
+ */
366
+ getAllItems() {
367
+ const disabled = loadDisabledAgents();
368
+ const agents = getBuiltInAgents().filter(a => !disabled.has(a.id));
369
+
370
+ const userSnippets = loadSnippets()
371
+ .filter(s => !s.hidden)
372
+ .map(s => ({
373
+ ...s,
374
+ type: 'snippet'
375
+ }));
376
+
377
+ return [...agents, ...userSnippets];
378
+ }
379
+
380
+ /**
381
+ * Fuzzy match score between query and text
382
+ */
383
+ _fuzzyScore(text, query) {
384
+ if (!query) return 1;
385
+
386
+ const textLower = text.toLowerCase();
387
+ const queryLower = query.toLowerCase();
388
+
389
+ if (textLower.startsWith(queryLower)) {
390
+ return 100 + (queryLower.length * 10);
391
+ }
392
+
393
+ const words = textLower.split(/[\s-_]/);
394
+ for (const word of words) {
395
+ if (word.startsWith(queryLower)) {
396
+ return 80 + (queryLower.length * 5);
397
+ }
398
+ }
399
+
400
+ let score = 0;
401
+ let textIdx = 0;
402
+ let prevMatchIdx = -1;
403
+ let consecutiveBonus = 0;
404
+
405
+ for (const char of queryLower) {
406
+ const idx = textLower.indexOf(char, textIdx);
407
+ if (idx === -1) return 0;
408
+
409
+ if (prevMatchIdx !== -1 && idx === prevMatchIdx + 1) {
410
+ consecutiveBonus += 2;
411
+ score += 5 + consecutiveBonus;
412
+ } else {
413
+ consecutiveBonus = 0;
414
+ }
415
+
416
+ score += 1;
417
+ prevMatchIdx = idx;
418
+ textIdx = idx + 1;
419
+ }
420
+
421
+ return score;
422
+ }
423
+
424
+ /**
425
+ * Search items with fuzzy matching
426
+ */
427
+ search(query) {
428
+ const items = this.getAllItems();
429
+
430
+ if (!query) {
431
+ return items.slice(0, 12);
432
+ }
433
+
434
+ const scored = items
435
+ .map(item => {
436
+ const nameScore = this._fuzzyScore(item.name, query);
437
+ const descScore = this._fuzzyScore(item.desc || '', query) * 0.5;
438
+ const score = Math.max(nameScore, descScore);
439
+
440
+ if (score === 0) return null;
441
+ return { ...item, score };
442
+ })
443
+ .filter(Boolean);
444
+
445
+ scored.sort((a, b) => b.score - a.score);
446
+
447
+ return scored.slice(0, 10);
448
+ }
449
+
450
+ /**
451
+ * Show the autocomplete dropdown
452
+ */
453
+ show(query, triggerPos) {
454
+ this.triggerPos = triggerPos;
455
+ this.query = query;
456
+
457
+ this.items = this.search(query);
458
+
459
+ if (this.items.length === 0) {
460
+ this.hide();
461
+ return;
462
+ }
463
+
464
+ this.selectedIndex = this.noPreselectOnce ? -1 : 0;
465
+ this.noPreselectOnce = false;
466
+ this.visible = true;
467
+ this.render();
468
+ this.container.classList.add('visible');
469
+ this.container.scrollTop = 0;
470
+ this._detachAnchor?.();
471
+ this._detachAnchor = anchorAbove(this.container, this.input, triggerPos);
472
+ window.addEventListener('keydown', this._handleModifier, true);
473
+ window.addEventListener('keyup', this._handleModifier, true);
474
+ }
475
+
476
+ /**
477
+ * Hide the autocomplete dropdown
478
+ */
479
+ hide() {
480
+ this.visible = false;
481
+ this.container.classList.remove('visible', 'mod-alt', 'mod-ctrl');
482
+ this._currentMod = '';
483
+ this.items = [];
484
+ this.selectedIndex = 0;
485
+ this.triggerPos = -1;
486
+ this.query = '';
487
+ this._detachAnchor?.();
488
+ this._detachAnchor = null;
489
+ window.removeEventListener('keydown', this._handleModifier, true);
490
+ window.removeEventListener('keyup', this._handleModifier, true);
491
+ }
492
+
493
+ /**
494
+ * Render the dropdown
495
+ */
496
+ render() {
497
+ const hasAgents = this.items.some(i => i.type === 'agent');
498
+ const hasSnippets = this.items.some(i => i.type === 'snippet');
499
+ const showSnippetsHeader = hasSnippets || loadSnippets().length === 0;
500
+
501
+ let html = '';
502
+
503
+ if (hasAgents) {
504
+ html += '<div class="snippets-section"><span>Agents</span><a class="snippets-edit-link" href="#" data-widget="agents">Edit</a></div>';
505
+ this.items.forEach((item, i) => {
506
+ if (item.type !== 'agent') return;
507
+ html += this._renderItem(item, i);
508
+ });
509
+ }
510
+
511
+ if (showSnippetsHeader) {
512
+ html += '<div class="snippets-section"><span>Snippets</span><a class="snippets-edit-link" href="#" data-widget="snippets">Edit</a></div>';
513
+ this.items.forEach((item, i) => {
514
+ if (item.type !== 'snippet') return;
515
+ html += this._renderItem(item, i);
516
+ });
517
+ }
518
+
519
+ if (!hasAgents && !showSnippetsHeader) {
520
+ this.items.forEach((item, i) => {
521
+ html += this._renderItem(item, i);
522
+ });
523
+ }
524
+
525
+ this.container.innerHTML = html;
526
+ }
527
+
528
+ /**
529
+ * Render a single item
530
+ */
531
+ _renderItem(item, index) {
532
+ const highlighted = this.highlightMatch(item.name, this.query);
533
+ let typeBadge = '';
534
+ if (item.type === 'agent' && item.source === 'project') {
535
+ typeBadge = '<span class="snippets-type-badge project">project</span>';
536
+ }
537
+
538
+ return `
539
+ <div class="snippets-item ${index === this.selectedIndex ? 'selected' : ''}"
540
+ data-index="${index}">
541
+ <div class="snippets-content">
542
+ <span class="snippets-name">${highlighted}</span>
543
+ ${item.desc ? `<span class="snippets-desc">${escapeHtml(item.desc)}</span>` : ''}
544
+ </div>
545
+ ${typeBadge}
546
+ <div class="snippets-actions">
547
+ <button class="snippets-insert-btn" data-index="${index}" data-tooltip="Insert (Enter)">
548
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
549
+ <polyline points="9 10 4 15 9 20"/>
550
+ <path d="M20 4v7a4 4 0 0 1-4 4H4"/>
551
+ </svg>
552
+ </button>
553
+ <button class="snippets-insertname-btn" data-index="${index}" data-tooltip="Insert name only (Alt+Enter)">
554
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
555
+ <polyline points="4 7 4 4 20 4 20 7"/>
556
+ <line x1="9" y1="20" x2="15" y2="20"/>
557
+ <line x1="12" y1="4" x2="12" y2="20"/>
558
+ </svg>
559
+ </button>
560
+ <button class="snippets-send-btn" data-index="${index}" data-tooltip="Send now (Ctrl+Enter)">
561
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
562
+ <line x1="22" y1="2" x2="11" y2="13"/>
563
+ <polygon points="22 2 15 22 11 13 2 9 22 2"/>
564
+ </svg>
565
+ </button>
566
+ </div>
567
+ </div>
568
+ `;
569
+ }
570
+
571
+ /**
572
+ * Highlight matching characters
573
+ */
574
+ highlightMatch(text, query) {
575
+ if (!query) return escapeHtml(text);
576
+
577
+ const textLower = text.toLowerCase();
578
+ const queryLower = query.toLowerCase();
579
+
580
+ let result = '';
581
+ let textIdx = 0;
582
+ let queryIdx = 0;
583
+
584
+ while (textIdx < text.length) {
585
+ if (queryIdx < queryLower.length && textLower[textIdx] === queryLower[queryIdx]) {
586
+ result += `<mark>${escapeHtml(text[textIdx])}</mark>`;
587
+ queryIdx++;
588
+ } else {
589
+ result += escapeHtml(text[textIdx]);
590
+ }
591
+ textIdx++;
592
+ }
593
+
594
+ return result;
595
+ }
596
+
597
+ /**
598
+ * Move selection up or down
599
+ */
600
+ moveSelection(delta) {
601
+ if (!this.visible || this.items.length === 0) return;
602
+
603
+ this.selectedIndex += delta;
604
+ if (this.selectedIndex < 0) this.selectedIndex = this.items.length - 1;
605
+ if (this.selectedIndex >= this.items.length) this.selectedIndex = 0;
606
+
607
+ this.render();
608
+
609
+ const selectedEl = this.container.querySelector('.snippets-item.selected');
610
+ if (selectedEl) {
611
+ selectedEl.scrollIntoView({ block: 'nearest' });
612
+ }
613
+ }
614
+
615
+ /**
616
+ * Select the current item
617
+ * @param {number} index - Item index to select
618
+ * @param {object} options - { short: true } to insert just the name instead of full pattern
619
+ */
620
+ select(index = this.selectedIndex, { short = false } = {}) {
621
+ if (index < 0 || index >= this.items.length) return;
622
+
623
+ const item = this.items[index];
624
+
625
+ let insertText;
626
+ if (short) {
627
+ insertText = item.name + ' ';
628
+ } else if (item.type === 'agent') {
629
+ const pattern = getAgentPattern(item.id);
630
+ insertText = pattern.replace('{agent}', item.name);
631
+ if (!insertText.endsWith(' ')) {
632
+ insertText += ' ';
633
+ }
634
+ } else {
635
+ insertText = (item.text || item.name) + ' ';
636
+ }
637
+
638
+ const value = this.input.value;
639
+ const before = value.slice(0, this.triggerPos);
640
+ const after = value.slice(this.triggerPos + 1 + this.query.length);
641
+
642
+ this.input.value = before + insertText + after;
643
+ this.input.focus();
644
+
645
+ const cursorPos = before.length + insertText.length;
646
+ this.input.selectionStart = this.input.selectionEnd = cursorPos;
647
+
648
+ this.hide();
649
+
650
+ this.input.dispatchEvent(new Event('input', { bubbles: true }));
651
+ }
652
+
653
+ /**
654
+ * Handle click on item
655
+ */
656
+ _handleClick(e) {
657
+ const editLink = e.target.closest('.snippets-edit-link');
658
+ if (editLink) {
659
+ e.preventDefault();
660
+ e.stopPropagation();
661
+ this.hide();
662
+ const widgetId = editLink.dataset.widget || 'snippets';
663
+ window.WidgetManager?.open(widgetId);
664
+ return;
665
+ }
666
+
667
+ const sendBtn = e.target.closest('.snippets-send-btn');
668
+ if (sendBtn) {
669
+ e.stopPropagation();
670
+ const index = parseInt(sendBtn.dataset.index, 10);
671
+ this.selectAndSend(index);
672
+ return;
673
+ }
674
+
675
+ const insertNameBtn = e.target.closest('.snippets-insertname-btn');
676
+ if (insertNameBtn) {
677
+ e.stopPropagation();
678
+ const index = parseInt(insertNameBtn.dataset.index, 10);
679
+ this.select(index, { short: true });
680
+ return;
681
+ }
682
+
683
+ const item = e.target.closest('.snippets-item');
684
+ if (item) {
685
+ const index = parseInt(item.dataset.index, 10);
686
+ this.select(index);
687
+ }
688
+ }
689
+
690
+ /**
691
+ * Select an item and immediately send it
692
+ */
693
+ selectAndSend(index = this.selectedIndex) {
694
+ if (index < 0 || index >= this.items.length) return;
695
+
696
+ const item = this.items[index];
697
+
698
+ let sendText;
699
+ if (item.type === 'agent') {
700
+ const pattern = getAgentPattern(item.id);
701
+ sendText = pattern.replace('{agent}', item.name);
702
+ if (!sendText.endsWith(' ')) {
703
+ sendText += ' ';
704
+ }
705
+ } else {
706
+ sendText = (item.text || item.name);
707
+ }
708
+
709
+ const value = this.input.value;
710
+ const before = value.slice(0, this.triggerPos);
711
+ const after = value.slice(this.triggerPos + 1 + this.query.length);
712
+ this.input.value = before + after;
713
+
714
+ this.hide();
715
+
716
+ if (window.app) {
717
+ const remainingText = this.input.value.trim();
718
+ const finalMessage = remainingText ? `${sendText.trim()} ${remainingText}` : sendText.trim();
719
+ this.input.value = '';
720
+ window.app.sendMessage(finalMessage);
721
+ }
722
+ }
723
+
724
+ /**
725
+ * Check if there's a valid selection
726
+ */
727
+ hasSelection() {
728
+ return this.selectedIndex >= 0 && this.selectedIndex < this.items.length;
729
+ }
730
+
731
+ /**
732
+ * Get the currently selected item
733
+ */
734
+ getSelectedItem() {
735
+ if (!this.hasSelection()) return null;
736
+ return this.items[this.selectedIndex];
737
+ }
738
+ }