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,1239 @@
1
+ /**
2
+ * Quick Actions Registry
3
+ *
4
+ * Central registry for all quick actions available in the radial menu.
5
+ * Actions are organized by category and include metadata for display,
6
+ * execution handlers, and state checks.
7
+ *
8
+ * Each action has:
9
+ * - id: Unique identifier
10
+ * - icon: Feather icon name (or SVG path)
11
+ * - label: Short display name
12
+ * - description: Tooltip/help text
13
+ * - category: Grouping for settings UI
14
+ * - shortcut: Optional keyboard chord, written as its Ctrl/Alt form. NEVER
15
+ * render this raw — read `shortcutDisplay`, which resolves it against the
16
+ * real shortcut registry so Mac shows the ⌘ binding that actually fires.
17
+ * - execute: Function to run the action
18
+ * - isEnabled: Optional function returning boolean
19
+ * - isVisible: Optional function returning boolean
20
+ * - badge: Optional function returning badge text/count
21
+ */
22
+
23
+ import { WidgetManager } from './widget-system/index.js';
24
+ import { DebugWidget } from './widgets/debug-widget.js';
25
+ import { TerminalWidget } from './widgets/terminal-widget.js';
26
+ import { appConfirm, escapeHtml, terminalAvailable } from './utils.js';
27
+ import { copyToClipboard, showToast } from './context-menu.js';
28
+ import { OpenDialog } from './open-dialog.js';
29
+ import S from './strings.js';
30
+ import { debug } from './config.js';
31
+ import { engineInfo } from './status-bar.js';
32
+ import { formatLiteralChord } from './shortcuts.js';
33
+
34
+ // ─────────────────────────────────────────────────────────────────────────────
35
+ // Registry Class
36
+ // ─────────────────────────────────────────────────────────────────────────────
37
+
38
+ class QuickActionsRegistryClass {
39
+ constructor() {
40
+ this.actions = new Map();
41
+ this.categories = new Map();
42
+ }
43
+
44
+ /**
45
+ * Register an action
46
+ */
47
+ register(id, config) {
48
+ const action = {
49
+ id,
50
+ icon: config.icon,
51
+ label: config.label,
52
+ description: config.description || config.label,
53
+ shortcut: config.shortcut || null,
54
+ // Lazy so user rebinds land without re-registering every action.
55
+ get shortcutDisplay() { return formatLiteralChord(this.shortcut); },
56
+ category: config.category,
57
+ keywords: config.keywords || [],
58
+ execute: config.execute,
59
+ isEnabled: config.isEnabled || (() => true),
60
+ isVisible: config.isVisible || (() => true),
61
+ badge: config.badge || null,
62
+ };
63
+
64
+ this.actions.set(id, action);
65
+
66
+ // Track categories
67
+ if (!this.categories.has(config.category)) {
68
+ this.categories.set(config.category, []);
69
+ }
70
+ this.categories.get(config.category).push(action);
71
+
72
+ return this;
73
+ }
74
+
75
+ /**
76
+ * Unregister an action (used to re-sync user-defined custom actions)
77
+ */
78
+ unregister(id) {
79
+ const action = this.actions.get(id);
80
+ if (!action) return false;
81
+ this.actions.delete(id);
82
+
83
+ const list = this.categories.get(action.category);
84
+ if (list) {
85
+ const idx = list.indexOf(action);
86
+ if (idx !== -1) list.splice(idx, 1);
87
+ if (list.length === 0) this.categories.delete(action.category);
88
+ }
89
+ return true;
90
+ }
91
+
92
+ /**
93
+ * Get an action by ID
94
+ */
95
+ get(id) {
96
+ return this.actions.get(id);
97
+ }
98
+
99
+ /**
100
+ * Get all actions in a category
101
+ */
102
+ getByCategory(category) {
103
+ return this.categories.get(category) || [];
104
+ }
105
+
106
+ /**
107
+ * Get all category names
108
+ */
109
+ getAllCategories() {
110
+ return Array.from(this.categories.keys());
111
+ }
112
+
113
+ /**
114
+ * Get all actions
115
+ */
116
+ getAll() {
117
+ return Array.from(this.actions.values());
118
+ }
119
+
120
+ /**
121
+ * Execute an action by ID
122
+ */
123
+ execute(id) {
124
+ const action = this.actions.get(id);
125
+ if (!action) {
126
+ console.warn(`[QuickActions] Unknown action: ${id}`);
127
+ return false;
128
+ }
129
+
130
+ if (!action.isEnabled()) {
131
+ debug.log(`[QuickActions] Action disabled: ${id}`);
132
+ return false;
133
+ }
134
+
135
+ try {
136
+ action.execute();
137
+ return true;
138
+ } catch (err) {
139
+ console.error(`[QuickActions] Error executing ${id}:`, err);
140
+ return false;
141
+ }
142
+ }
143
+ }
144
+
145
+ // Singleton instance
146
+ export const QuickActionsRegistry = new QuickActionsRegistryClass();
147
+
148
+ // ─────────────────────────────────────────────────────────────────────────────
149
+ // Helper to get app instance
150
+ // ─────────────────────────────────────────────────────────────────────────────
151
+
152
+ function getApp() {
153
+ return window.app;
154
+ }
155
+
156
+ // ─────────────────────────────────────────────────────────────────────────────
157
+ // Category: Sessions & Tabs
158
+ // ─────────────────────────────────────────────────────────────────────────────
159
+
160
+ QuickActionsRegistry.register('new-session', {
161
+ keywords: ['create', 'add', 'fresh', 'start', 'tab', 'launch'],
162
+ icon: 'plus-circle',
163
+ label: S.quick_actions_registry.actions.new_session.label,
164
+ description: S.quick_actions_registry.actions.new_session.desc,
165
+ shortcut: 'Ctrl+K',
166
+ category: S.quick_actions_registry.categories.sessions,
167
+ execute: () => getApp()?.createSession(),
168
+ isEnabled: () => (getApp()?.sessionManager?.sessions?.length || 0) < 10,
169
+ });
170
+
171
+ QuickActionsRegistry.register('close-tab', {
172
+ keywords: ['x', 'remove', 'kill', 'dismiss', 'shut'],
173
+ icon: 'x',
174
+ label: S.quick_actions_registry.actions.close_tab.label,
175
+ description: S.quick_actions_registry.actions.close_tab.desc,
176
+ shortcut: 'Escape',
177
+ category: S.quick_actions_registry.categories.sessions,
178
+ execute: () => {
179
+ const app = getApp();
180
+ if (!app) return;
181
+
182
+ // First try to close topmost floating widget (like ESC key behavior)
183
+ const { WidgetManager } = window;
184
+ if (WidgetManager?.closeTopmost?.({ allowTerminalPassthrough: false })) {
185
+ return; // Widget was closed, done
186
+ }
187
+
188
+ // No widgets open - close the current tab based on active mode
189
+ if (app.activeMode === 'widget' && app.activeWidgetTabId) {
190
+ app.closeWidgetTab(app.activeWidgetTabId);
191
+ } else if (app.activeSession) {
192
+ app.closeSession(app.activeSession);
193
+ }
194
+ },
195
+ isEnabled: () => {
196
+ const app = getApp();
197
+ if (!app) return false;
198
+ // Enable if there's any widget or tab to close
199
+ const hasWidget = window.WidgetManager?.hasVisibleWidgets?.();
200
+ return hasWidget ||
201
+ app.activeWidgetTabId ||
202
+ app.sessionManager?.sessions?.length > 0;
203
+ },
204
+ });
205
+
206
+ QuickActionsRegistry.register('prev-tab', {
207
+ keywords: ['back', 'left', 'before'],
208
+ icon: 'chevron-left',
209
+ label: S.quick_actions_registry.actions.prev_tab.label,
210
+ description: S.quick_actions_registry.actions.prev_tab.desc,
211
+ shortcut: 'Ctrl+[',
212
+ category: S.quick_actions_registry.categories.sessions,
213
+ execute: () => getApp()?.cycleTab(-1),
214
+ isEnabled: () => (getApp()?.sessionManager?.sessions?.length || 0) > 1,
215
+ });
216
+
217
+ QuickActionsRegistry.register('next-tab', {
218
+ keywords: ['forward', 'right', 'after'],
219
+ icon: 'chevron-right',
220
+ label: S.quick_actions_registry.actions.next_tab.label,
221
+ description: S.quick_actions_registry.actions.next_tab.desc,
222
+ shortcut: 'Ctrl+]',
223
+ category: S.quick_actions_registry.categories.sessions,
224
+ execute: () => getApp()?.cycleTab(1),
225
+ isEnabled: () => (getApp()?.sessionManager?.sessions?.length || 0) > 1,
226
+ });
227
+
228
+ QuickActionsRegistry.register('fork-session', {
229
+ keywords: ['branch', 'split', 'diverge'],
230
+ icon: 'git-branch',
231
+ label: S.quick_actions_registry.actions.fork_session.label,
232
+ description: S.quick_actions_registry.actions.fork_session.desc,
233
+ category: S.quick_actions_registry.categories.sessions,
234
+ execute: () => getApp()?.forkSession?.(),
235
+ // Hidden for engines that can't branch a conversation (capabilities.fork=false)
236
+ isEnabled: () => getApp()?.activeSession?.messages?.length > 0
237
+ && getApp()?.activeSession?.providerCaps?.fork !== false,
238
+ });
239
+
240
+ QuickActionsRegistry.register('clone-session', {
241
+ keywords: ['duplicate', 'copy', 'same project'],
242
+ icon: 'copy',
243
+ label: S.quick_actions_registry.actions.clone_session.label,
244
+ description: S.quick_actions_registry.actions.clone_session.desc,
245
+ shortcut: 'Ctrl+Shift+N',
246
+ category: S.quick_actions_registry.categories.sessions,
247
+ execute: () => getApp()?.cloneSession?.(),
248
+ isEnabled: () => getApp()?.activeSession?.cwd != null,
249
+ });
250
+
251
+ QuickActionsRegistry.register('continue-in-cli', {
252
+ keywords: ['shell', 'bash', 'cli', 'resume terminal'],
253
+ icon: 'terminal',
254
+ label: S.quick_actions_registry.actions.continue_in_cli.label,
255
+ description: S.quick_actions_registry.actions.continue_in_cli.desc,
256
+ category: S.quick_actions_registry.categories.sessions,
257
+ execute: async () => {
258
+ const session = getApp()?.activeSession;
259
+ if (!session?.providerSessionId) return;
260
+ // Engine-self-described resume command (Claude "claude -r {id}", Codex
261
+ // "codex exec resume {id}") — never hardcode an engine's resume verb.
262
+ const engine = engineInfo(session.provider || session.pendingProvider);
263
+ const tmpl = engine?.cli_resume_template || 'claude -r {id}';
264
+ const cmd = tmpl.replace('{id}', session.providerSessionId);
265
+ if (await copyToClipboard(cmd)) {
266
+ const shown = tmpl.replace('{id}', session.providerSessionId.slice(0, 8) + '…');
267
+ showToast(S.toast.cli_resume_copied.replace('{cmd}', shown));
268
+ }
269
+ },
270
+ isEnabled: () => !!getApp()?.activeSession?.providerSessionId,
271
+ });
272
+
273
+ QuickActionsRegistry.register('new-draft', {
274
+ keywords: ['scratch', 'note', 'draft', 'temp'],
275
+ icon: 'edit',
276
+ label: S.quick_actions_registry.actions.new_draft.label,
277
+ description: S.quick_actions_registry.actions.new_draft.desc,
278
+ shortcut: 'Ctrl+N',
279
+ category: S.quick_actions_registry.categories.sessions,
280
+ execute: () => getApp()?.createScratchTab(),
281
+ });
282
+
283
+ QuickActionsRegistry.register('toggle-favorite', {
284
+ keywords: ['star', 'pin', 'bookmark', 'mark'],
285
+ icon: 'star',
286
+ label: S.quick_actions_registry.actions.toggle_favorite.label,
287
+ description: S.quick_actions_registry.actions.toggle_favorite.desc,
288
+ category: S.quick_actions_registry.categories.sessions,
289
+ execute: async () => {
290
+ const app = getApp();
291
+ const session = app?.activeSession;
292
+ if (!session?.storeId) return;
293
+
294
+ // Dynamic import to avoid circular dependency
295
+ const { toggleFavoriteSession } = await import('./welcome.js');
296
+ const newState = await toggleFavoriteSession(session.storeId);
297
+
298
+ // Show feedback
299
+
300
+ if (newState === null) {
301
+ showToast(S.errors.update_favorite);
302
+ } else {
303
+ showToast(newState ? S.toast.added_favorite : S.toast.removed_favorite);
304
+ }
305
+ },
306
+ isEnabled: () => {
307
+ const session = getApp()?.activeSession;
308
+ return session?.storeId != null;
309
+ },
310
+ badge: () => {
311
+ // Could show star if favorited, but checking synchronously is tricky
312
+ // For now, no badge - the icon itself is a star
313
+ return null;
314
+ },
315
+ });
316
+
317
+ // ─────────────────────────────────────────────────────────────────────────────
318
+ // Category: Panels & Widgets
319
+ // ─────────────────────────────────────────────────────────────────────────────
320
+
321
+ // Terminal actions register only when the server has a PTY backend
322
+ // (absent on native Windows until the ConPTY port lands).
323
+ if (terminalAvailable()) {
324
+ QuickActionsRegistry.register('terminal', {
325
+ keywords: ['shell', 'bash', 'console', 'cli', 'tty'],
326
+ icon: 'terminal',
327
+ label: S.quick_actions_registry.actions.terminal.label,
328
+ description: S.quick_actions_registry.actions.terminal.desc,
329
+ shortcut: 'Ctrl+`',
330
+ category: S.quick_actions_registry.categories.panels,
331
+ execute: () => getApp()?.toggleTerminalPanel(),
332
+ });
333
+
334
+ QuickActionsRegistry.register('new-terminal', {
335
+ keywords: ['shell', 'console', 'pty', 'tty'],
336
+ icon: 'plus-square',
337
+ label: S.quick_actions_registry.actions.new_terminal.label,
338
+ description: S.quick_actions_registry.actions.new_terminal.desc,
339
+ shortcut: 'Ctrl+Shift+`',
340
+ category: S.quick_actions_registry.categories.panels,
341
+ execute: () => getApp()?.createTerminal(),
342
+ });
343
+ }
344
+
345
+ QuickActionsRegistry.register('file-preview', {
346
+ keywords: ['preview', 'view', 'file', 'editor', 'reopen', 'last'],
347
+ icon: 'eye',
348
+ label: S.quick_actions_registry.actions.file_preview.label,
349
+ description: S.quick_actions_registry.actions.file_preview.desc,
350
+ shortcut: 'Alt+V',
351
+ category: S.quick_actions_registry.categories.panels,
352
+ execute: () => getApp()?.togglePreview(),
353
+ });
354
+
355
+ QuickActionsRegistry.register('file-explorer', {
356
+ keywords: ['files', 'browser', 'tree', 'folder', 'directory', 'dir'],
357
+ icon: 'folder',
358
+ label: S.quick_actions_registry.actions.file_explorer.label,
359
+ description: S.quick_actions_registry.actions.file_explorer.desc,
360
+ shortcut: 'Alt+F',
361
+ category: S.quick_actions_registry.categories.panels,
362
+ execute: () => getApp()?.toggleFileExplorer(),
363
+ });
364
+
365
+ QuickActionsRegistry.register('search-files', {
366
+ keywords: ['grep', 'find', 'text', 'content', 'search in files', 'ripgrep', 'project search'],
367
+ icon: 'search',
368
+ label: S.quick_actions_registry.actions.search_files.label,
369
+ description: S.quick_actions_registry.actions.search_files.desc,
370
+ shortcut: 'Ctrl+Shift+F',
371
+ category: S.quick_actions_registry.categories.panels,
372
+ execute: () => getApp()?.toggleSearchFiles(),
373
+ });
374
+
375
+ QuickActionsRegistry.register('log-explorer', {
376
+ keywords: ['logs', 'output', 'journal', 'transcript'],
377
+ icon: 'scroll',
378
+ label: S.quick_actions_registry.actions.log_explorer.label,
379
+ description: S.quick_actions_registry.actions.log_explorer.desc,
380
+ shortcut: 'Alt+L',
381
+ category: S.quick_actions_registry.categories.panels,
382
+ execute: () => getApp()?.toggleLogExplorer(),
383
+ });
384
+
385
+ QuickActionsRegistry.register('git-panel', {
386
+ keywords: ['vcs', 'repo', 'commit', 'status', 'branch', 'diff'],
387
+ icon: 'git-merge',
388
+ label: S.quick_actions_registry.actions.git_panel.label,
389
+ description: S.quick_actions_registry.actions.git_panel.desc,
390
+ shortcut: 'Alt+G',
391
+ category: S.quick_actions_registry.categories.panels,
392
+ execute: () => getApp()?.toggleGitPanel(),
393
+ });
394
+
395
+ QuickActionsRegistry.register('cost-analytics', {
396
+ keywords: ['money', 'spend', 'tokens', 'billing', 'usage', 'price', 'analytics'],
397
+ icon: 'coins',
398
+ label: S.quick_actions_registry.actions.cost_analytics.label,
399
+ description: S.quick_actions_registry.actions.cost_analytics.desc,
400
+ shortcut: 'Alt+4',
401
+ category: S.quick_actions_registry.categories.panels,
402
+ execute: () => getApp()?.toggleCostAnalytics(),
403
+ });
404
+
405
+ QuickActionsRegistry.register('active-sessions', {
406
+ keywords: ['running', 'live', 'busy', 'workers'],
407
+ icon: 'activity',
408
+ label: S.quick_actions_registry.actions.active_sessions.label,
409
+ description: S.quick_actions_registry.actions.active_sessions.desc,
410
+ shortcut: 'Alt+S',
411
+ category: S.quick_actions_registry.categories.panels,
412
+ execute: () => getApp()?.toggleActiveSessions(),
413
+ });
414
+
415
+ QuickActionsRegistry.register('discussion', {
416
+ keywords: ['ask', 'thread', 'qa', 'forked talk', 'question'],
417
+ icon: 'message-circle',
418
+ label: S.quick_actions_registry.actions.discussion.label,
419
+ description: S.quick_actions_registry.actions.discussion.desc,
420
+ shortcut: 'Alt+/',
421
+ category: S.quick_actions_registry.categories.panels,
422
+ execute: () => getApp()?.toggleDiscussion(),
423
+ });
424
+
425
+ QuickActionsRegistry.register('debug-logs', {
426
+ keywords: ['errors', 'console', 'browser', 'devtools'],
427
+ icon: 'bug',
428
+ label: S.quick_actions_registry.actions.debug_console.label,
429
+ description: S.quick_actions_registry.actions.debug_console.desc,
430
+ shortcut: 'Alt+D',
431
+ category: S.quick_actions_registry.categories.panels,
432
+ execute: () => WidgetManager.toggle('debug-logs'),
433
+ badge: () => {
434
+ const n = DebugWidget.getUnseenErrorCount();
435
+ return n > 0 ? (n > 99 ? '99+' : String(n)) : null;
436
+ },
437
+ });
438
+
439
+ QuickActionsRegistry.register('prompt-history', {
440
+ keywords: ['recent', 'past', 'previous prompts', 'archive', 'search prompts'],
441
+ icon: 'clock',
442
+ label: S.quick_actions_registry.actions.prompt_history.label,
443
+ description: S.quick_actions_registry.actions.prompt_history.desc,
444
+ shortcut: 'Ctrl+R',
445
+ category: S.quick_actions_registry.categories.panels,
446
+ execute: () => WidgetManager.toggle('prompt-explorer'),
447
+ });
448
+
449
+ QuickActionsRegistry.register('skills', {
450
+ keywords: ['skill', 'skills', 'manage skills', 'folder skill', 'SKILL.md'],
451
+ icon: 'dollarSign',
452
+ label: S.quick_actions_registry.actions.skills.label,
453
+ description: S.quick_actions_registry.actions.skills.desc,
454
+ shortcut: 'Alt+K',
455
+ category: S.quick_actions_registry.categories.panels,
456
+ execute: () => WidgetManager.toggle('skills'),
457
+ });
458
+
459
+ QuickActionsRegistry.register('commands', {
460
+ keywords: ['command', 'commands', 'slash', 'slash commands', 'cli', 'builtin', 'legacy'],
461
+ icon: 'chevron-right',
462
+ label: S.quick_actions_registry.actions.commands.label,
463
+ description: S.quick_actions_registry.actions.commands.desc,
464
+ shortcut: 'Alt+Shift+K',
465
+ category: S.quick_actions_registry.categories.panels,
466
+ execute: () => WidgetManager.toggle('commands'),
467
+ });
468
+
469
+ QuickActionsRegistry.register('agents', {
470
+ keywords: ['agent', 'agents', 'subagent', 'manage agents', 'agent definition'],
471
+ icon: 'brain',
472
+ label: S.quick_actions_registry.actions.agents.label,
473
+ description: S.quick_actions_registry.actions.agents.desc,
474
+ category: S.quick_actions_registry.categories.panels,
475
+ execute: () => WidgetManager.toggle('agents'),
476
+ });
477
+
478
+ QuickActionsRegistry.register('plugins', {
479
+ keywords: ['plugin', 'plugins', 'marketplace', 'install', 'uninstall', 'enable'],
480
+ icon: 'tool',
481
+ label: S.quick_actions_registry.actions.plugins.label,
482
+ description: S.quick_actions_registry.actions.plugins.desc,
483
+ category: S.quick_actions_registry.categories.panels,
484
+ execute: () => WidgetManager.toggle('plugins'),
485
+ });
486
+
487
+ QuickActionsRegistry.register('browser', {
488
+ keywords: ['web', 'url', 'website', 'iframe', 'html', 'render', 'webview', 'page'],
489
+ icon: 'globe',
490
+ label: S.quick_actions_registry.actions.browser.label,
491
+ description: S.quick_actions_registry.actions.browser.desc,
492
+ shortcut: 'Alt+B',
493
+ category: S.quick_actions_registry.categories.panels,
494
+ execute: () => WidgetManager.toggle('browser'),
495
+ });
496
+
497
+ QuickActionsRegistry.register('snippets', {
498
+ keywords: ['snippet', 'snippets', 'expand', 'expansion', 'macro', 'shortcut text'],
499
+ icon: 'code',
500
+ label: S.quick_actions_registry.actions.snippets.label,
501
+ description: S.quick_actions_registry.actions.snippets.desc,
502
+ category: S.quick_actions_registry.categories.panels,
503
+ execute: () => WidgetManager.toggle('snippets'),
504
+ });
505
+
506
+ QuickActionsRegistry.register('uploads-browser', {
507
+ keywords: ['files', 'images', 'pictures', 'attachments', 'media'],
508
+ icon: 'image',
509
+ label: S.quick_actions_registry.actions.uploads_browser.label,
510
+ description: S.quick_actions_registry.actions.uploads_browser.desc,
511
+ category: S.quick_actions_registry.categories.panels,
512
+ execute: () => WidgetManager.toggle('uploads'),
513
+ });
514
+
515
+ QuickActionsRegistry.register('background-tasks', {
516
+ keywords: ['jobs', 'workers', 'async', 'parallel', 'queue'],
517
+ icon: 'loader',
518
+ label: S.quick_actions_registry.actions.background_tasks.label,
519
+ description: S.quick_actions_registry.actions.background_tasks.desc,
520
+ category: S.quick_actions_registry.categories.panels,
521
+ execute: () => WidgetManager.toggle('background-tasks'),
522
+ });
523
+
524
+ QuickActionsRegistry.register('history-explorer', {
525
+ keywords: ['journal', 'past', 'archive', 'old', 'turns'],
526
+ icon: 'archive',
527
+ label: S.quick_actions_registry.actions.history_explorer.label,
528
+ description: S.quick_actions_registry.actions.history_explorer.desc,
529
+ shortcut: 'Alt+H',
530
+ category: S.quick_actions_registry.categories.panels,
531
+ execute: () => getApp()?.toggleHistoryExplorer(),
532
+ });
533
+
534
+ QuickActionsRegistry.register('effort-settings', {
535
+ keywords: ['thinking', 'reasoning', 'depth', 'budget', 'low high', 'ultrathink'],
536
+ icon: 'cpu',
537
+ label: 'Cycle Effort Level',
538
+ description: 'Cycle through effort levels (low/medium/high/xhigh/max)',
539
+ shortcut: "Ctrl+'",
540
+ category: S.quick_actions_registry.categories.panels,
541
+ execute: () => getApp()?.toggleThinkingSettings(),
542
+ });
543
+
544
+ QuickActionsRegistry.register('effort-oneshot', {
545
+ keywords: ['one-shot', 'oneshot', 'next prompt', 'temporary', 'bump', 'boost'],
546
+ icon: 'zap',
547
+ label: 'Cycle Effort One-Shot',
548
+ description: 'Arm a one-shot effort override for the next send only, then auto-reverts',
549
+ shortcut: "Ctrl+Shift+'",
550
+ category: S.quick_actions_registry.categories.panels,
551
+ execute: () => getApp()?.cycleEffortOneShot(),
552
+ });
553
+
554
+ QuickActionsRegistry.register('reopen-tab', {
555
+ keywords: ['restore', 'undo close', 'last closed', 'recover'],
556
+ icon: 'rotate-ccw',
557
+ label: S.quick_actions_registry.actions.reopen_tab.label,
558
+ description: S.quick_actions_registry.actions.reopen_tab.desc,
559
+ shortcut: 'Ctrl+Shift+T',
560
+ category: S.quick_actions_registry.categories.sessions,
561
+ execute: () => getApp()?.reopenLastClosedTab(),
562
+ });
563
+
564
+ QuickActionsRegistry.register('quick-switcher', {
565
+ keywords: ['palette', 'command palette', 'launcher', 'spotlight', 'fzf', 'cmd+p', 'ctrl+p', 'finder'],
566
+ icon: 'command',
567
+ label: S.quick_switcher.action.label,
568
+ description: S.quick_switcher.action.desc,
569
+ shortcut: 'Ctrl+K',
570
+ category: S.quick_actions_registry.categories.panels,
571
+ execute: () => getApp()?.toggleQuickSwitcher(),
572
+ });
573
+
574
+ QuickActionsRegistry.register('open-dialog', {
575
+ keywords: ['open file', 'open folder', 'open directory', 'path', 'browse', 'navigate', 'tab complete', 'cmd+o'],
576
+ icon: 'folder',
577
+ label: S.open_dialog.action.label,
578
+ description: S.open_dialog.action.desc,
579
+ shortcut: 'Ctrl+O',
580
+ category: S.quick_actions_registry.categories.panels,
581
+ execute: () => getApp()?.toggleOpenDialog(),
582
+ });
583
+
584
+ QuickActionsRegistry.register('settings', {
585
+ keywords: ['preferences', 'config', 'options', 'prefs'],
586
+ icon: 'settings',
587
+ label: S.quick_actions_registry.actions.settings.label,
588
+ description: S.quick_actions_registry.actions.settings.desc,
589
+ shortcut: 'Ctrl+,',
590
+ category: S.quick_actions_registry.categories.panels,
591
+ execute: () => getApp()?.showSettings(),
592
+ });
593
+
594
+ QuickActionsRegistry.register('about', {
595
+ keywords: ['version', 'license', 'agpl', 'source', 'github', 'credits', 'info'],
596
+ icon: 'info',
597
+ label: S.quick_actions_registry.actions.about.label,
598
+ description: S.quick_actions_registry.actions.about.desc,
599
+ category: S.quick_actions_registry.categories.panels,
600
+ execute: () => WidgetManager.open('about'),
601
+ });
602
+
603
+ QuickActionsRegistry.register('help', {
604
+ keywords: ['docs', 'shortcuts', 'tips', 'manual'],
605
+ icon: 'help-circle',
606
+ label: S.quick_actions_registry.actions.help.label,
607
+ description: S.quick_actions_registry.actions.help.desc,
608
+ category: S.quick_actions_registry.categories.panels,
609
+ execute: () => getApp()?.showHelp(),
610
+ });
611
+
612
+ // ─────────────────────────────────────────────────────────────────────────────
613
+ // Category: Chat Actions
614
+ // ─────────────────────────────────────────────────────────────────────────────
615
+
616
+ QuickActionsRegistry.register('send-message', {
617
+ keywords: ['submit', 'go', 'enter', 'ship'],
618
+ icon: 'send',
619
+ label: S.quick_actions_registry.actions.send_message.label,
620
+ description: S.quick_actions_registry.actions.send_message.desc,
621
+ shortcut: 'Ctrl+Enter',
622
+ category: S.quick_actions_registry.categories.chat,
623
+ execute: () => {
624
+ const app = getApp();
625
+ if (!app) return;
626
+ // During a working turn, send-btn is hidden and followup-btn is the visible target.
627
+ const followupBtn = document.getElementById('followup-btn');
628
+ const sendBtn = document.getElementById('send-btn');
629
+ const target = (followupBtn && followupBtn.classList.contains('visible') && !followupBtn.disabled)
630
+ ? followupBtn
631
+ : (sendBtn && !sendBtn.disabled ? sendBtn : null);
632
+ if (target) {
633
+ target.click();
634
+ } else if (app.sendMessage) {
635
+ app.sendMessage();
636
+ }
637
+ },
638
+ isEnabled: () => {
639
+ const app = getApp();
640
+ if (!app) return false;
641
+ const input = app.els?.messageInput;
642
+ return input && input.value.trim().length > 0;
643
+ },
644
+ });
645
+
646
+ QuickActionsRegistry.register('stop', {
647
+ keywords: ['cancel', 'abort', 'kill', 'interrupt', 'halt'],
648
+ icon: 'square',
649
+ label: S.quick_actions_registry.actions.stop.label,
650
+ description: S.quick_actions_registry.actions.stop.desc,
651
+ shortcut: 'Escape',
652
+ category: S.quick_actions_registry.categories.chat,
653
+ execute: () => getApp()?.stopClaude?.(),
654
+ isEnabled: () => getApp()?.isTyping === true,
655
+ badge: () => getApp()?.isTyping ? '!' : null,
656
+ });
657
+
658
+ QuickActionsRegistry.register('scroll-bottom', {
659
+ keywords: ['end', 'last', 'down', 'newest'],
660
+ icon: 'chevrons-down',
661
+ label: S.quick_actions_registry.actions.scroll_bottom.label,
662
+ description: S.quick_actions_registry.actions.scroll_bottom.desc,
663
+ category: S.quick_actions_registry.categories.chat,
664
+ execute: () => {
665
+ const container = document.querySelector('.messages-container');
666
+ if (container) container.scrollTop = container.scrollHeight;
667
+ },
668
+ });
669
+
670
+ QuickActionsRegistry.register('scroll-top', {
671
+ keywords: ['begin', 'first', 'up', 'oldest'],
672
+ icon: 'chevrons-up',
673
+ label: S.quick_actions_registry.actions.scroll_top.label,
674
+ description: S.quick_actions_registry.actions.scroll_top.desc,
675
+ category: S.quick_actions_registry.categories.chat,
676
+ execute: () => {
677
+ const container = document.querySelector('.messages-container');
678
+ if (container) container.scrollTop = 0;
679
+ },
680
+ });
681
+
682
+ QuickActionsRegistry.register('focus-input', {
683
+ keywords: ['type', 'compose', 'message box', 'caret'],
684
+ icon: 'edit',
685
+ label: S.quick_actions_registry.actions.focus_input.label,
686
+ description: S.quick_actions_registry.actions.focus_input.desc,
687
+ shortcut: 'Ctrl+/',
688
+ category: S.quick_actions_registry.categories.chat,
689
+ execute: () => getApp()?.focusInput(),
690
+ });
691
+
692
+ QuickActionsRegistry.register('search-chat', {
693
+ keywords: ['find', 'lookup', 'search messages', 'in conversation'],
694
+ icon: 'search',
695
+ label: S.quick_actions_registry.actions.search_chat.label,
696
+ description: S.quick_actions_registry.actions.search_chat.desc,
697
+ // Real binding is Ctrl+F (shortcuts.js 'search') — Ctrl+Shift+F now
698
+ // belongs to the project-wide Search in Files widget.
699
+ shortcut: 'Ctrl+F',
700
+ category: S.quick_actions_registry.categories.chat,
701
+ execute: () => getApp()?.openSearch(),
702
+ });
703
+
704
+ QuickActionsRegistry.register('trim-messages', {
705
+ keywords: ['compact', 'reduce', 'cleanup', 'shrink', 'free memory'],
706
+ icon: 'minimize-2',
707
+ label: S.quick_actions_registry.actions.trim_messages.label,
708
+ description: S.quick_actions_registry.actions.trim_messages.desc,
709
+ category: S.quick_actions_registry.categories.chat,
710
+ execute: () => getApp()?.handleTrimMessages(),
711
+ isEnabled: () => (getApp()?.chatCtrl?.getTrimCount() || 0) > 0, // Uses default 100, preserves last turn
712
+ badge: () => {
713
+ const count = getApp()?.chatCtrl?.getTrimCount() || 0; // Uses default 100, preserves last turn
714
+ return count > 0 ? (count > 99 ? '99+' : String(count)) : null;
715
+ },
716
+ });
717
+
718
+ // ─────────────────────────────────────────────────────────────────────────────
719
+ // Category: Input Actions
720
+ // ─────────────────────────────────────────────────────────────────────────────
721
+
722
+ QuickActionsRegistry.register('upload', {
723
+ keywords: ['attach', 'add file', 'browse', 'image'],
724
+ icon: 'upload',
725
+ label: S.quick_actions_registry.actions.upload.label,
726
+ description: S.quick_actions_registry.actions.upload.desc,
727
+ category: S.quick_actions_registry.categories.input,
728
+ execute: () => getApp()?.uploadManager?.openFilePicker(),
729
+ });
730
+
731
+ QuickActionsRegistry.register('paste', {
732
+ keywords: ['clipboard', 'insert'],
733
+ icon: 'clipboard',
734
+ label: S.quick_actions_registry.actions.paste.label,
735
+ description: S.quick_actions_registry.actions.paste.desc,
736
+ category: S.quick_actions_registry.categories.input,
737
+ execute: async () => {
738
+ const app = getApp();
739
+ if (!app) return;
740
+
741
+ // If a terminal is recently focused or is the active tab, paste there
742
+ if (TerminalWidget.isRecentlyFocused()) {
743
+ const handled = await TerminalWidget.paste();
744
+ if (handled) return;
745
+ }
746
+
747
+ try {
748
+ // Try to read clipboard (requires permission on some browsers)
749
+ if (navigator.clipboard && navigator.clipboard.read) {
750
+ const items = await navigator.clipboard.read();
751
+
752
+ for (const item of items) {
753
+ // Check for image types first
754
+ const imageType = item.types.find(t => t.startsWith('image/'));
755
+ if (imageType) {
756
+ const blob = await item.getType(imageType);
757
+ // Use uploadManager if available
758
+ if (app.uploadManager?.handlePastedImage) {
759
+ app.uploadManager.handlePastedImage(blob);
760
+ // Refocus input to keep keyboard open
761
+ app.els?.messageInput?.focus();
762
+ return;
763
+ }
764
+ // Fallback: create a paste event-like handling
765
+ const file = new File([blob], 'pasted-image.png', { type: imageType });
766
+ if (app.uploadManager?.handleFiles) {
767
+ app.uploadManager.handleFiles([file]);
768
+ // Refocus input to keep keyboard open
769
+ app.els?.messageInput?.focus();
770
+ return;
771
+ }
772
+ }
773
+
774
+ // Check for text
775
+ if (item.types.includes('text/plain')) {
776
+ const blob = await item.getType('text/plain');
777
+ const text = await blob.text();
778
+
779
+ // Insert text at cursor in message input
780
+ const input = app.els?.messageInput;
781
+ if (input) {
782
+ const start = input.selectionStart;
783
+ const end = input.selectionEnd;
784
+ const before = input.value.substring(0, start);
785
+ const after = input.value.substring(end);
786
+ input.value = before + text + after;
787
+ input.selectionStart = input.selectionEnd = start + text.length;
788
+ input.focus();
789
+ // Trigger input event for auto-resize
790
+ input.dispatchEvent(new Event('input', { bubbles: true }));
791
+ }
792
+ return;
793
+ }
794
+ }
795
+ } else if (navigator.clipboard && navigator.clipboard.readText) {
796
+ // Fallback for browsers that only support readText
797
+ const text = await navigator.clipboard.readText();
798
+ const input = app.els?.messageInput;
799
+ if (input && text) {
800
+ const start = input.selectionStart;
801
+ const end = input.selectionEnd;
802
+ const before = input.value.substring(0, start);
803
+ const after = input.value.substring(end);
804
+ input.value = before + text + after;
805
+ input.selectionStart = input.selectionEnd = start + text.length;
806
+ input.focus();
807
+ input.dispatchEvent(new Event('input', { bubbles: true }));
808
+ }
809
+ } else {
810
+ // Last resort: focus input and let user paste manually
811
+ app.els?.messageInput?.focus();
812
+ debug.log('[QuickActions] Clipboard API not available, focused input for manual paste');
813
+ }
814
+ } catch (err) {
815
+ // Permission denied or other error - focus input as fallback
816
+ console.warn('[QuickActions] Clipboard access failed:', err.message);
817
+ app.els?.messageInput?.focus();
818
+ // Show hint to user
819
+ app.activeSession?.addSystemLog(S.quick_actions_registry.paste_hint, 'info');
820
+ }
821
+ },
822
+ });
823
+
824
+ // ─────────────────────────────────────────────────────────────────────────────
825
+ // Category: Navigation
826
+ // ─────────────────────────────────────────────────────────────────────────────
827
+
828
+ QuickActionsRegistry.register('change-cwd', {
829
+ keywords: ['working directory', 'project', 'folder', 'path', 'dir', 'cd'],
830
+ icon: 'folder-open',
831
+ label: S.quick_actions_registry.actions.change_cwd.label,
832
+ description: S.quick_actions_registry.actions.change_cwd.desc,
833
+ category: S.quick_actions_registry.categories.navigation,
834
+ execute: () => {
835
+ OpenDialog.show();
836
+ },
837
+ });
838
+
839
+ // ─────────────────────────────────────────────────────────────────────────────
840
+ // Category: Utilities
841
+ // ─────────────────────────────────────────────────────────────────────────────
842
+
843
+ QuickActionsRegistry.register('storage-info', {
844
+ keywords: ['localstorage', 'usage', 'space', 'quota', 'disk'],
845
+ icon: 'database',
846
+ label: S.quick_actions_registry.actions.storage_info.label,
847
+ description: S.quick_actions_registry.actions.storage_info.desc,
848
+ category: S.quick_actions_registry.categories.utilities,
849
+ execute: async () => {
850
+ const { Storage } = await import('./utils.js');
851
+
852
+ const usage = Storage.getUsage();
853
+ const sorted = Object.entries(usage.keys).sort((a, b) => b[1] - a[1]).slice(0, 5);
854
+ const lines = sorted.map(([key, bytes]) => {
855
+ const fmt = bytes > 1024 * 1024
856
+ ? (bytes / 1024 / 1024).toFixed(1) + 'M'
857
+ : (bytes / 1024).toFixed(0) + 'K';
858
+ return `${key}: ${fmt}`;
859
+ });
860
+ showToast(`Storage: ${usage.usedMB}\n${lines.join('\n')}`, 4000);
861
+ },
862
+ });
863
+
864
+ QuickActionsRegistry.register('storage-cleanup', {
865
+ keywords: ['clean', 'free space', 'reduce', 'trim', 'gc', 'garbage'],
866
+ icon: 'trash',
867
+ label: S.quick_actions_registry.actions.storage_cleanup.label,
868
+ description: S.quick_actions_registry.actions.storage_cleanup.desc,
869
+ category: S.quick_actions_registry.categories.utilities,
870
+ execute: async () => {
871
+ const { Storage } = await import('./utils.js');
872
+
873
+ const app = getApp();
874
+ if (!app?.sessionManager) return;
875
+
876
+ const beforeUsage = Storage.getUsage();
877
+
878
+ for (const session of app.sessionManager.sessions) {
879
+ if (session.messages.length > 30) {
880
+ session.messages = session.messages.slice(-30);
881
+ }
882
+ }
883
+ app.sessionManager.saveSessions();
884
+
885
+ const afterUsage = Storage.getUsage();
886
+ const saved = beforeUsage.used - afterUsage.used;
887
+ const savedFmt = saved > 1024 * 1024
888
+ ? (saved / 1024 / 1024).toFixed(2) + ' MB'
889
+ : (saved / 1024).toFixed(1) + ' KB';
890
+ showToast(`Freed ${savedFmt} of storage. Now ${afterUsage.usedMB}.`, 3000);
891
+ },
892
+ });
893
+
894
+ QuickActionsRegistry.register('fullscreen', {
895
+ keywords: ['expand', 'maximize', 'f11', 'full screen'],
896
+ icon: 'maximize',
897
+ label: S.quick_actions_registry.actions.fullscreen.label,
898
+ description: S.quick_actions_registry.actions.fullscreen.desc,
899
+ category: S.quick_actions_registry.categories.utilities,
900
+ execute: () => {
901
+ if (document.fullscreenElement) {
902
+ document.exitFullscreen();
903
+ } else {
904
+ document.documentElement.requestFullscreen();
905
+ }
906
+ },
907
+ });
908
+
909
+ QuickActionsRegistry.register('reload', {
910
+ keywords: ['refresh', 'f5', 'restart page'],
911
+ icon: 'refresh-cw',
912
+ label: S.quick_actions_registry.actions.reload.label,
913
+ description: S.quick_actions_registry.actions.reload.desc,
914
+ category: S.quick_actions_registry.categories.utilities,
915
+ execute: () => location.reload(),
916
+ });
917
+
918
+ QuickActionsRegistry.register('hard-reset', {
919
+ keywords: ['nuke', 'wipe', 'clear cache', 'fresh', 'factory reset'],
920
+ icon: 'trash-2',
921
+ label: S.quick_actions_registry.actions.hard_reset.label,
922
+ description: S.quick_actions_registry.actions.hard_reset.desc,
923
+ category: S.quick_actions_registry.categories.utilities,
924
+ execute: async () => {
925
+ const ok = await appConfirm(S.quick_actions_registry.hard_reset_confirm, { confirmLabel: 'Reset', danger: true });
926
+ if (!ok) return;
927
+ // Clear service workers
928
+ if ('serviceWorker' in navigator) {
929
+ const regs = await navigator.serviceWorker.getRegistrations();
930
+ await Promise.all(regs.map(r => r.unregister()));
931
+ }
932
+ // Clear browser caches
933
+ if ('caches' in window) {
934
+ const names = await caches.keys();
935
+ await Promise.all(names.map(n => caches.delete(n)));
936
+ }
937
+ // Clear all localStorage (sessions, tabs, messages, settings)
938
+ localStorage.clear();
939
+ // Clear sessionStorage
940
+ sessionStorage.clear();
941
+ location.reload(true);
942
+ },
943
+ });
944
+
945
+ QuickActionsRegistry.register('capture-snapshot', {
946
+ keywords: ['debug', 'dump', 'export state', 'localstorage', 'screenshot state'],
947
+ icon: 'camera',
948
+ label: S.quick_actions_registry.actions.capture_snapshot.label,
949
+ description: S.quick_actions_registry.actions.capture_snapshot.desc,
950
+ category: S.quick_actions_registry.categories.utilities,
951
+ execute: async () => {
952
+ const snapshot = {
953
+ version: 2,
954
+ captured_at: new Date().toISOString(),
955
+ url: location.href,
956
+ localStorage: {},
957
+ metrics: {
958
+ dom_nodes: document.querySelectorAll('*').length,
959
+ heap_mb: performance.memory
960
+ ? Math.round(performance.memory.usedJSHeapSize / 1024 / 1024 * 100) / 100
961
+ : null,
962
+ session_count: window.app?.sessionManager?.sessions?.length || 0,
963
+ total_in_memory_messages:
964
+ (window.app?.sessionManager?.sessions || []).reduce((sum, s) => sum + (s.messages?.length || 0), 0),
965
+ },
966
+ session_summary: [],
967
+ memory_state: {
968
+ active_session_id: window.app?.activeSession?.id || null,
969
+ active_mode: localStorage.getItem('claude-code-active-mode') || 'session',
970
+ sessions: (window.app?.sessionManager?.sessions || []).map(s => ({
971
+ id: s.id,
972
+ storeId: s.storeId || null,
973
+ in_memory_message_count: s.messages?.length || 0,
974
+ has_more_messages: !!s.hasMoreMessages,
975
+ total_message_count: s.totalMessageCount || 0,
976
+ has_cached_dom: !!window.app?.chatCtrl?.sessionPool?.containers?.has(s.id),
977
+ })),
978
+ },
979
+ };
980
+ for (let i = 0; i < localStorage.length; i++) {
981
+ const k = localStorage.key(i);
982
+ snapshot.localStorage[k] = localStorage.getItem(k);
983
+ }
984
+ try {
985
+ const sessions = JSON.parse(localStorage.getItem('claude-code-sessions') || '[]');
986
+ snapshot.session_summary = sessions.map(s => ({
987
+ id: s.id, name: s.name, cwd: s.cwd,
988
+ message_count: (s.messages || []).length,
989
+ total_cost: s.totalCost || 0, model: s.model,
990
+ has_store_id: !!s.storeId,
991
+ store_id: s.storeId || null,
992
+ }));
993
+ } catch (e) { snapshot.parse_error = e.message; }
994
+ let bytes = 0;
995
+ for (const [k, v] of Object.entries(snapshot.localStorage))
996
+ bytes += k.length + (v ? v.length : 0);
997
+ snapshot.localStorage_bytes = bytes;
998
+
999
+ // ── Client-side full memory dump ──
1000
+ // Captures the FULL un-truncated in-memory state per session
1001
+ // (localStorage only has 30 truncated messages per session)
1002
+ const clientSessions = {};
1003
+ let clientBytes = 0;
1004
+ const MAX_CLIENT_BYTES = 30 * 1024 * 1024; // 30MB cap
1005
+ for (const s of (window.app?.sessionManager?.sessions || [])) {
1006
+ const sessionData = {
1007
+ id: s.id,
1008
+ storeId: s.storeId || null,
1009
+ name: s.name,
1010
+ cwd: s.cwd,
1011
+ // Full un-truncated messages (the critical piece)
1012
+ messages: s.messages || [],
1013
+ message_count: s.messages?.length || 0,
1014
+ // Runtime state
1015
+ runtime: {
1016
+ turnId: s.turnId || 0,
1017
+ model: s.model,
1018
+ providerSessionId: s.providerSessionId,
1019
+ contextTokens: s.contextTokens || 0,
1020
+ contextWindow: s.contextWindow || 0,
1021
+ totalInputTokens: s.totalInputTokens || 0,
1022
+ totalOutputTokens: s.totalOutputTokens || 0,
1023
+ totalCost: s.totalCost || 0,
1024
+ hasMoreMessages: !!s.hasMoreMessages,
1025
+ totalMessageCount: s.totalMessageCount || 0,
1026
+ permissionMode: s.permissionMode || null,
1027
+ isAgentRunning: !!s.isAgentRunning,
1028
+ isReady: !!s.isReady,
1029
+ },
1030
+ // Transient UI state
1031
+ scrollPosition: s.scrollPosition,
1032
+ isUserScrolledUp: !!s.isUserScrolledUp,
1033
+ inputText: s.inputText || '',
1034
+ // System logs (connection events, errors)
1035
+ systemLogs: (s.systemLogs || []).slice(-100), // last 100
1036
+ // Prompt history
1037
+ promptHistory: s.promptHistory || [],
1038
+ };
1039
+ const chunk = JSON.stringify(sessionData);
1040
+ if (clientBytes + chunk.length > MAX_CLIENT_BYTES) break;
1041
+ clientBytes += chunk.length;
1042
+ clientSessions[s.id] = sessionData;
1043
+ }
1044
+ snapshot.client_sessions = clientSessions;
1045
+ snapshot.client_sessions_bytes = clientBytes;
1046
+ snapshot.metrics.client_sessions_count = Object.keys(clientSessions).length;
1047
+ snapshot.metrics.total_client_messages =
1048
+ Object.values(clientSessions).reduce((sum, s) => sum + (s.message_count || 0), 0);
1049
+
1050
+ const numSessions = snapshot.session_summary.length;
1051
+ const totalMsgs = snapshot.session_summary.reduce((s, x) => s + x.message_count, 0);
1052
+ const sizeKB = (bytes / 1024).toFixed(1);
1053
+
1054
+ try {
1055
+ const resp = await fetch('/api/perf/snapshot', {
1056
+ method: 'POST',
1057
+ headers: { 'Content-Type': 'application/json' },
1058
+ body: JSON.stringify(snapshot),
1059
+ });
1060
+ if (resp.status === 401) {
1061
+ // auth-fetch wrapper is already redirecting to /login. Bail
1062
+ // quietly — don't queue an "upload failed" toast that would
1063
+ // flash after the user logs back in.
1064
+ return;
1065
+ }
1066
+ if (!resp.ok) {
1067
+ throw new Error(`HTTP ${resp.status}`);
1068
+ }
1069
+ const result = await resp.json();
1070
+
1071
+ const path = result.path;
1072
+ const serverInfo = result.server_sessions
1073
+ ? `+ ${result.server_sessions} server sessions (${result.server_bytes_mb || 0}MB)`
1074
+ : '';
1075
+ const clientInfo = snapshot.metrics.total_client_messages
1076
+ ? `${snapshot.metrics.total_client_messages} client msgs (${(clientBytes / 1024 / 1024).toFixed(1)}MB)`
1077
+ : '';
1078
+ showToast(`
1079
+ <div style="font-weight:600;margin-bottom:4px">Snapshot saved</div>
1080
+ <div class="snapshot-toast-path" data-tooltip="Tap to copy">${escapeHtml(path)}</div>
1081
+ <div style="margin-top:4px;opacity:.7;font-size:12px">
1082
+ ${numSessions} sessions, ${totalMsgs} msgs, ${sizeKB} KB, ${snapshot.metrics.dom_nodes} DOM nodes
1083
+ ${clientInfo ? `<br>${clientInfo}` : ''}
1084
+ ${serverInfo ? `<br>${serverInfo}` : ''}
1085
+ </div>
1086
+ `, {
1087
+ duration: 8000,
1088
+ html: true,
1089
+ pauseOnHover: true,
1090
+ interactive: true,
1091
+ className: 'snapshot-toast',
1092
+ onMount: (toast) => {
1093
+ const pathEl = toast.querySelector('.snapshot-toast-path');
1094
+ pathEl.addEventListener('click', async (e) => {
1095
+ e.stopPropagation();
1096
+ try {
1097
+ await navigator.clipboard.writeText(path);
1098
+ pathEl.textContent = 'Copied!';
1099
+ setTimeout(() => { pathEl.textContent = path; }, 1000);
1100
+ } catch {
1101
+ const range = document.createRange();
1102
+ range.selectNodeContents(pathEl);
1103
+ const sel = window.getSelection();
1104
+ sel.removeAllRanges();
1105
+ sel.addRange(range);
1106
+ }
1107
+ });
1108
+ },
1109
+ });
1110
+ } catch (e) {
1111
+ // Don't attempt a blob download fallback: iPadOS WKWebView ignores
1112
+ // the `download` attribute on programmatic <a> clicks and navigates
1113
+ // the PWA to the blob: URL instead, breaking the app shell.
1114
+ console.error('[capture-snapshot] upload failed:', e);
1115
+ showToast(S.toast.upload_failed, 3000);
1116
+ }
1117
+ },
1118
+ });
1119
+
1120
+ QuickActionsRegistry.register('zen-mode', {
1121
+ keywords: ['focus', 'distraction free', 'minimal', 'hide ui', 'clean'],
1122
+ icon: 'moon',
1123
+ label: S.quick_actions_registry.actions.zen_mode.label,
1124
+ description: S.quick_actions_registry.actions.zen_mode.desc,
1125
+ shortcut: 'Alt+Z',
1126
+ category: S.quick_actions_registry.categories.panels,
1127
+ execute: () => {
1128
+ window.toggleZenMode?.();
1129
+ },
1130
+ isEnabled: () => !!getApp()?.activeSession,
1131
+ });
1132
+
1133
+ // ─────────────────────────────────────────────────────────────────────────────
1134
+ // Custom user-defined actions — terminal command / prompt / slash command
1135
+ //
1136
+ // Definitions live in global config (`quickActions.customActions`, synced
1137
+ // cross-device) and are (re-)registered here by syncCustomActions(), called
1138
+ // from quick-actions-menu's load/save paths. Shape:
1139
+ // { id: 'custom-<rand>', label, icon, type: 'terminal'|'prompt'|'slash',
1140
+ // payload: string }
1141
+ // ─────────────────────────────────────────────────────────────────────────────
1142
+
1143
+ export const CUSTOM_ACTION_TYPES = ['terminal', 'prompt', 'slash'];
1144
+
1145
+ function executeCustomAction(def) {
1146
+ const app = getApp();
1147
+ if (!app) return;
1148
+ const payload = (def.payload || '').trim();
1149
+ if (!payload) return;
1150
+
1151
+ switch (def.type) {
1152
+ case 'terminal':
1153
+ // Open a terminal tab in the active session's cwd and run it
1154
+ app.tabCtrl?.openTerminalWidgetTab({
1155
+ cwd: app.activeSession?.cwd || null,
1156
+ title: def.label,
1157
+ initialCommand: payload + '\n',
1158
+ });
1159
+ break;
1160
+ case 'prompt':
1161
+ // Send through the normal message path (history, stash, welcome routing)
1162
+ app.sendMessage(payload);
1163
+ break;
1164
+ case 'slash':
1165
+ app.handleSlashCommand(payload.startsWith('/') ? payload : '/' + payload);
1166
+ break;
1167
+ default:
1168
+ console.warn('[QuickActions] Unknown custom action type:', def.type);
1169
+ }
1170
+ }
1171
+
1172
+ /**
1173
+ * Replace all registered custom actions with the given definitions.
1174
+ * Idempotent — safe to call on every config load/save.
1175
+ */
1176
+ export function syncCustomActions(defs) {
1177
+ for (const action of QuickActionsRegistry.getAll()) {
1178
+ if (action.id.startsWith('custom-')) {
1179
+ QuickActionsRegistry.unregister(action.id);
1180
+ }
1181
+ }
1182
+
1183
+ for (const def of defs || []) {
1184
+ if (!def?.id || !def.label || !CUSTOM_ACTION_TYPES.includes(def.type)) continue;
1185
+ const typeName = S.settings.qa_editor.custom_types[def.type] || def.type;
1186
+ const payload = def.payload || '';
1187
+ const preview = payload.length > 60 ? payload.slice(0, 60) + '…' : payload;
1188
+ QuickActionsRegistry.register(def.id, {
1189
+ icon: def.icon || 'zap',
1190
+ label: def.label,
1191
+ description: `${typeName}: ${preview}`,
1192
+ category: S.quick_actions_registry.categories.custom,
1193
+ keywords: ['custom', def.type, def.label],
1194
+ execute: () => executeCustomAction(def),
1195
+ // Prompt/slash need a session to send into; terminal works anywhere
1196
+ isEnabled: def.type === 'terminal' ? undefined : () => !!getApp()?.activeSession,
1197
+ });
1198
+ }
1199
+ }
1200
+
1201
+ // ─────────────────────────────────────────────────────────────────────────────
1202
+ // Presets — loaded from server (/api/app/presets → ~/.painapple-code/presets/)
1203
+ // Source of truth: presets.defaults.json (ships with project, seeds user dir)
1204
+ // ─────────────────────────────────────────────────────────────────────────────
1205
+
1206
+ // Minimal fallback if server hasn't responded yet
1207
+ export const QUICK_ACTION_PRESETS = {
1208
+ balanced: {
1209
+ name: 'Balanced',
1210
+ description: S.quick_actions_registry.presets.balanced.desc,
1211
+ slots: ['git-panel', 'file-explorer', 'new-session', 'close-tab', 'stop', 'terminal']
1212
+ }
1213
+ };
1214
+
1215
+ /**
1216
+ * Replace presets with server-provided values (from ~/.painapple-code/presets/*.json).
1217
+ */
1218
+ export function updatePresets(serverPresets) {
1219
+ for (const key of Object.keys(QUICK_ACTION_PRESETS)) {
1220
+ delete QUICK_ACTION_PRESETS[key];
1221
+ }
1222
+ Object.assign(QUICK_ACTION_PRESETS, serverPresets);
1223
+ }
1224
+
1225
+ // ─────────────────────────────────────────────────────────────────────────────
1226
+ // Default Configuration
1227
+ // ─────────────────────────────────────────────────────────────────────────────
1228
+
1229
+ export const DEFAULT_QUICK_ACTIONS_CONFIG = {
1230
+ preset: 'balanced',
1231
+ slots: QUICK_ACTION_PRESETS.balanced.slots,
1232
+ options: {
1233
+ contextAware: true,
1234
+ showTooltips: true,
1235
+ dragRelease: false,
1236
+ hapticFeedback: true,
1237
+ menuPosition: 'bottom-right'
1238
+ }
1239
+ };