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,574 @@
1
+ /**
2
+ * Init-UI mixin — one-time construction of the DOM element cache and all the
3
+ * sub-component instances (autocomplete, chat search/navigator, scroll manager,
4
+ * status bar, activity strip, input handler, the file/snippet/skill
5
+ * autocompletes), server-info fetch, and the left-rail buttons + drawer.
6
+ * Extracted from app.js; applied to App.prototype via Object.assign. Uses `this`
7
+ * (App instance) plus the imports below.
8
+ */
9
+ import S from '../strings.js';
10
+ import { $ } from '../utils.js';
11
+ import { CONFIG, HAS_PHYSICAL_KEYBOARD, debug, setServerHome, setServerWorkspace, setServerVersionInfo } from '../config.js';
12
+ import { AutocompleteUI } from '../components.js';
13
+ import { ChatSearch } from '../chat-search.js';
14
+ import { ChatNavigator } from '../chat-navigator.js';
15
+ import { ScrollManager } from '../scroll-manager.js';
16
+ import { StatusBar, ensureModelsLoaded } from '../status-bar.js';
17
+ import { ActivityStrip } from '../activity-strip.js';
18
+ import { InputHandler } from '../input-handler.js';
19
+ import { FileAutocomplete } from '../file-autocomplete.js';
20
+ import { SnippetsAutocomplete } from '../snippets-autocomplete.js';
21
+ import { SkillsAutocomplete } from '../skills-autocomplete.js';
22
+ import { WidgetManager, WidgetBus } from '../widget-system/init.js';
23
+ import { OpenDialog } from '../open-dialog.js';
24
+ import { layoutSwitcher } from '../layout-switcher.js';
25
+ import { withChords } from '../shortcuts.js';
26
+ import { state as configState } from '../widgets/config/state.js';
27
+
28
+ /**
29
+ * Rail button → the widget it shows, for the `.active` highlight.
30
+ *
31
+ * Lives here rather than on the widget configs so the rail stays the sole
32
+ * owner of its own visual state — widgets don't need to know a rail exists.
33
+ * Only entries whose target is a registered widget belong here; rail-search-btn
34
+ * (chat search) and the stateless buttons (layout, help) are handled separately.
35
+ */
36
+ const RAIL_WIDGET_BUTTONS = {
37
+ 'rail-sessions-btn': 'active-sessions',
38
+ 'rail-agents-btn': 'sub-agents',
39
+ 'rail-bg-tasks-btn': 'background-tasks',
40
+ 'rail-files-btn': 'file-explorer',
41
+ 'rail-preview-btn': 'file-preview',
42
+ 'rail-logs-btn': 'log-explorer',
43
+ 'rail-terminal-btn': 'terminal',
44
+ 'rail-git-btn': 'git',
45
+ 'rail-browser-btn': 'browser',
46
+ 'rail-history-btn': 'history-explorer',
47
+ 'rail-skills-btn': 'skills',
48
+ 'rail-agent-defs-btn': 'agents',
49
+ 'rail-snippets-btn': 'snippets',
50
+ 'rail-discussion-btn': 'discussion',
51
+ 'rail-cost-btn': 'cost-analytics',
52
+ 'rail-settings-btn': 'config',
53
+ };
54
+
55
+ export const initUiMethods = {
56
+ initElements() {
57
+ this.els = {
58
+ tabs: $('#tabs'),
59
+ tabsViewport: $('.tabs-viewport'),
60
+ newTabBtn: $('#new-tab-btn'),
61
+ messages: $('#messages'),
62
+ messagesContainer: $('#messages-container'),
63
+ activityStrip: $('#activity-strip'),
64
+ inputContainer: $('#input-container'),
65
+ inputResizeHandle: $('#input-resize-handle'),
66
+ messageInput: $('#message-input'),
67
+ historyRefsHint: $('#history-refs-hint'),
68
+ sendBtn: $('#send-btn'),
69
+ followupBtn: $('#followup-btn'),
70
+ stopBtn: $('#stop-btn'),
71
+ connectionBar: $('#connection-bar'),
72
+ inputArea: $('#input-area'),
73
+ openFolderBtn: $('#open-folder-btn'),
74
+ modalOverlay: $('#modal-overlay'),
75
+ modalBody: $('.modal-body'),
76
+ modalClose: $('#modal-close'),
77
+ confirmDialog: $('#confirm-dialog'),
78
+ confirmPath: $('#confirm-path'),
79
+ confirmCancel: $('#confirm-cancel'),
80
+ confirmCreate: $('#confirm-create'),
81
+ statusConnection: $('#status-connection'),
82
+ statusEngine: $('#status-engine'),
83
+ statusModel: $('#status-model'),
84
+ statusBranch: $('#status-branch'),
85
+ statusProject: $('#status-project'),
86
+ statusTokens: $('#status-tokens'),
87
+ statusCost: $('#status-cost'),
88
+ uploadBtn: $('#upload-btn'),
89
+ promptHistoryBtn: $('#prompt-history-btn'),
90
+ uploadsBtn: $('#uploads-btn'),
91
+ discussionBtn: $('#discussion-btn'),
92
+ fileMentionBtn: $('#file-mention-btn'),
93
+ snippetsBtn: $('#snippets-btn'),
94
+ slashCmdBtn: $('#slash-cmd-btn'),
95
+ skillsBtn: $('#skills-btn'),
96
+ chatSearchBar: $('#chat-search-bar'),
97
+ chatSearchInput: $('#chat-search-input'),
98
+ chatSearchCount: $('#chat-search-count'),
99
+ chatSearchPrev: $('#chat-search-prev'),
100
+ chatSearchNext: $('#chat-search-next'),
101
+ chatSearchClose: $('#chat-search-close'),
102
+ // View containers
103
+ sessionView: $('#session-view'),
104
+ welcomeView: $('#welcome-view'),
105
+ };
106
+ },
107
+
108
+ initAutocomplete() {
109
+ this.autocomplete = new AutocompleteUI(
110
+ $('#autocomplete'),
111
+ this.els.messageInput
112
+ );
113
+
114
+ // Initialize file autocomplete (@-mentions)
115
+ this.fileAutocomplete = new FileAutocomplete(
116
+ this.els.messageInput,
117
+ { apiBase: CONFIG.API_BASE },
118
+ {
119
+ getCwd: () => this.activeSession?.cwd,
120
+ getChangedFiles: () => []
121
+ }
122
+ );
123
+
124
+ // Initialize snippets autocomplete (#-trigger for agents and snippets)
125
+ this.snippetsAutocomplete = new SnippetsAutocomplete(
126
+ this.els.messageInput
127
+ );
128
+
129
+ // Initialize skills autocomplete (~ trigger for inserting `/skill-name`)
130
+ this.skillsAutocomplete = new SkillsAutocomplete(
131
+ this.els.messageInput
132
+ );
133
+
134
+ // While a type-ahead picker (#, /, @, $) is open, tint its toolbar
135
+ // button with the accent color — same look as the Tab-cycle
136
+ // highlight — so typing the trigger char visibly "lights up" its
137
+ // icon. Every popup toggles `.visible` on its container, so a class
138
+ // observer catches all open/close paths (typed trigger, debounced
139
+ // show, Escape, selection commit) without touching the popups.
140
+ this._initTriggerButtonHighlights();
141
+
142
+ // The welcome / connection bar is a single button that opens the
143
+ // OpenDialog picker (which handles picking or creating a folder and
144
+ // connecting). Label comes from strings.js.
145
+ if (this.els.openFolderBtn) {
146
+ this.els.openFolderBtn.textContent = S.connection.open_folder;
147
+ this.els.openFolderBtn.addEventListener('click', () => OpenDialog.show());
148
+ }
149
+
150
+ // Initialize Chat Search (Ctrl+F in chat)
151
+ this.chatSearch = new ChatSearch(
152
+ this.els.messages,
153
+ {
154
+ searchBar: this.els.chatSearchBar,
155
+ searchInput: this.els.chatSearchInput,
156
+ countDisplay: this.els.chatSearchCount,
157
+ prevBtn: this.els.chatSearchPrev,
158
+ nextBtn: this.els.chatSearchNext,
159
+ closeBtn: this.els.chatSearchClose
160
+ },
161
+ {
162
+ onClose: () => this.els.messageInput?.focus()
163
+ }
164
+ );
165
+
166
+ // Initialize Scroll Manager (Slack-style new messages indicator)
167
+ this.scrollManager = new ScrollManager(
168
+ this.els.messagesContainer,
169
+ { scrollThreshold: 100, topThreshold: 100 },
170
+ {
171
+ onScrollTop: () => this.handleScrollTop(),
172
+ getSession: () => this.activeSession
173
+ }
174
+ );
175
+
176
+ // Initialize Chat Navigator (Cmd+Up/Down to jump between user messages)
177
+ this.chatNavigator = new ChatNavigator(
178
+ {
179
+ messagesContainer: this.els.messagesContainer,
180
+ messages: this.els.messages
181
+ },
182
+ {
183
+ getSession: () => this.activeSession,
184
+ onLoadMore: () => this.handleScrollTop()
185
+ }
186
+ );
187
+
188
+ // Initialize Status Bar (connection status, typing indicator)
189
+ this.statusBar = new StatusBar(
190
+ {
191
+ statusConnection: this.els.statusConnection,
192
+ statusEngine: this.els.statusEngine,
193
+ statusModel: this.els.statusModel,
194
+ statusBranch: this.els.statusBranch,
195
+ statusProject: this.els.statusProject,
196
+ statusCost: this.els.statusCost,
197
+ statusTokens: this.els.statusTokens,
198
+ sendBtn: this.els.sendBtn,
199
+ followupBtn: this.els.followupBtn,
200
+ stopBtn: this.els.stopBtn
201
+ },
202
+ {
203
+ getSession: () => this.activeSession
204
+ }
205
+ );
206
+ this.statusBar.init(); // Set up click handlers for token popover
207
+
208
+ // Warm the models cache so {model} placeholders render with the
209
+ // correct label on first paint (helpers modal, settings toggles).
210
+ ensureModelsLoaded();
211
+
212
+ // Activity Strip (between messages and input — peripheral, not competing with cursor)
213
+ this.activityStrip = new ActivityStrip(this.els.activityStrip, this.els.inputArea);
214
+
215
+ // Initialize Input Handler (history, drafts, input events)
216
+ this.inputHandler = new InputHandler(
217
+ {
218
+ messageInput: this.els.messageInput,
219
+ sendBtn: this.els.sendBtn,
220
+ followupBtn: this.els.followupBtn,
221
+ historyRefsHint: this.els.historyRefsHint
222
+ },
223
+ { hasPhysicalKeyboard: HAS_PHYSICAL_KEYBOARD },
224
+ {
225
+ getSession: () => this.activeSession,
226
+ getAutocomplete: () => this.autocomplete,
227
+ getFileAutocomplete: () => this.fileAutocomplete,
228
+ getSnippetsAutocomplete: () => this.snippetsAutocomplete,
229
+ getSkillsAutocomplete: () => this.skillsAutocomplete,
230
+ getCwd: () => this.activeSession?.cwd || null,
231
+ getPendingImages: () => this.pendingImages,
232
+ isUploading: () => this.uploadManager?.isUploading || false,
233
+ hasPendingUploads: () => this.uploadManager?.hasPending || false,
234
+ onSendMessage: (text, options) => this.sendMessage(text, options),
235
+ onSlashCommand: (cmd) => this.handleSlashCommand(cmd),
236
+ onBangCommand: (cmd) => this.handleBangCommand(cmd),
237
+ onPlanComposeChange: (active) => this.onPlanComposeChange(active),
238
+ onConnect: () => this.connectActiveSession(),
239
+ onAutoResize: () => { this.autoResizeInput(); this.syncInputHighlightBackdrop(); },
240
+ onUpdateSendButton: () => this.updateSendButtonState(),
241
+ onPreviewFile: (path) => this.previewFile(path),
242
+ onSendInNewSession: (text) => this.sendInNewSession(text),
243
+ isWelcomeMode: () => this.chatCtrl?.isWelcomeShowing() || false
244
+ }
245
+ );
246
+
247
+ // Type-anywhere search: typing on welcome screen focuses input
248
+ this._initTypeAnywhereSearch();
249
+
250
+ // Fetch server info first to get home path, then load directories
251
+ this.initServerInfo();
252
+ },
253
+
254
+ /**
255
+ * Accent-highlight a trigger's toolbar button while its picker is open.
256
+ * One class observer per popup container; the slash popup only lights
257
+ * the "/" button in slash mode (bang mode shares the same container
258
+ * but has no toolbar button).
259
+ */
260
+ _initTriggerButtonHighlights() {
261
+ const pairs = [
262
+ [this.snippetsAutocomplete, this.els.snippetsBtn],
263
+ [this.fileAutocomplete, this.els.fileMentionBtn],
264
+ [this.skillsAutocomplete, this.els.skillsBtn],
265
+ [this.autocomplete, this.els.slashCmdBtn,
266
+ () => this.autocomplete.mode === 'slash'],
267
+ ];
268
+ for (const [popup, btn, extra] of pairs) {
269
+ if (!popup?.container || !btn) continue;
270
+ const sync = () => btn.classList.toggle(
271
+ 'trigger-armed',
272
+ popup.container.classList.contains('visible') && (!extra || extra())
273
+ );
274
+ new MutationObserver(sync).observe(
275
+ popup.container, { attributes: true, attributeFilter: ['class'] }
276
+ );
277
+ sync();
278
+ }
279
+ },
280
+
281
+ /**
282
+ * Fetch server info (home path) before loading directories
283
+ */
284
+ async initServerInfo() {
285
+ try {
286
+ const response = await fetch(`${CONFIG.API_BASE}/api/info`);
287
+ const data = await response.json();
288
+ if (data.home) setServerHome(data.home);
289
+ if (data.workspace) setServerWorkspace(data.workspace);
290
+ setServerVersionInfo({
291
+ version: data.version,
292
+ staticBuild: data.static_build,
293
+ bundled: data.frontend_bundled,
294
+ bundleReason: data.frontend_reason,
295
+ diskVersion: data.disk_version,
296
+ restartNeeded: data.restart_needed,
297
+ license: data.license,
298
+ urls: data.urls,
299
+ author: data.author,
300
+ });
301
+ // Anchor the file explorer to the explicit workspace when set
302
+ // (the project base), falling back to the OS home otherwise.
303
+ const anchor = data.workspace || data.home;
304
+ if (anchor && this.fileExplorer) {
305
+ this.fileExplorer.setHomePath(anchor);
306
+ }
307
+ } catch (err) {
308
+ debug.warn('Failed to fetch server info:', err);
309
+ }
310
+ },
311
+
312
+ /**
313
+ * Initialize left rail buttons - mirror functionality from header/toolbar
314
+ * Rail provides quick access to common widgets on desktop
315
+ */
316
+ initLeftRailButtons() {
317
+ // Inject text labels for the expanded rail. The tooltip text is the
318
+ // single source for the label — read before refreshRailShortcutTooltips
319
+ // appends "(key)" (shortcutBase covers a re-run after that).
320
+ document.querySelectorAll('#left-rail .rail-btn').forEach(btn => {
321
+ if (btn.querySelector('.rail-label')) return;
322
+ const text = btn.dataset.shortcutBase || btn.getAttribute('data-tooltip');
323
+ if (!text) return;
324
+ const label = document.createElement('span');
325
+ label.className = 'rail-label';
326
+ label.textContent = text;
327
+ btn.insertBefore(label, btn.querySelector('.rail-badge'));
328
+ });
329
+
330
+ // Helper to bind rail button to action (click + touchend for iOS)
331
+ const bind = (id, action) => {
332
+ const btn = document.getElementById(id);
333
+ if (btn) {
334
+ btn.addEventListener('click', action);
335
+ btn.addEventListener('touchend', (e) => {
336
+ e.preventDefault();
337
+ action();
338
+ }, { passive: false });
339
+ }
340
+ };
341
+
342
+ // Activity / running things
343
+ bind('rail-sessions-btn', () => WidgetManager.toggle('active-sessions'));
344
+ bind('rail-agents-btn', () => WidgetManager.toggle('sub-agents'));
345
+ bind('rail-bg-tasks-btn', () => WidgetManager.toggle('background-tasks'));
346
+ // rail-orphan-terminals-btn is wired by orphan-terminals.js (uses #orphan-terminals-btn wrapper)
347
+
348
+ // Files / Code
349
+ // toggle(), not open(): the button now carries an .active highlight, so
350
+ // it has to behave like the toggle it looks like (and like its own
351
+ // Alt+F shortcut, which already toggled).
352
+ bind('rail-files-btn', () => this.fileExplorer?.toggle());
353
+ bind('rail-preview-btn', () => this.togglePreview());
354
+ bind('rail-logs-btn', () => WidgetManager.toggle('log-explorer'));
355
+ bind('rail-terminal-btn', () => this.toggleTerminalPanel());
356
+ bind('rail-git-btn', () => WidgetManager.toggle('git'));
357
+ bind('rail-browser-btn', () => WidgetManager.toggle('browser'));
358
+
359
+ // Find
360
+ bind('rail-search-btn', () => this.chatSearch?.toggle());
361
+ bind('rail-history-btn', () => WidgetManager.toggle('history-explorer'));
362
+
363
+ // Library (skills / agent definitions / snippets)
364
+ bind('rail-skills-btn', () => WidgetManager.toggle('skills'));
365
+ bind('rail-agent-defs-btn', () => WidgetManager.toggle('agents'));
366
+ bind('rail-snippets-btn', () => WidgetManager.toggle('snippets'));
367
+
368
+ // Discussion
369
+ bind('rail-discussion-btn', () => WidgetManager.toggle('discussion'));
370
+
371
+ // Bottom buttons
372
+ bind('rail-cost-btn', () => WidgetManager.toggle('cost-analytics'));
373
+ bind('rail-layout-btn', () => layoutSwitcher.toggle(document.getElementById('rail-layout-btn')));
374
+ bind('rail-settings-btn', () => this.showSettings());
375
+ bind('rail-help-btn', () => this.showHelp());
376
+
377
+ this.refreshRailShortcutTooltips();
378
+ this.initRailActiveState();
379
+ },
380
+
381
+ /**
382
+ * Keep the rail's `.active` highlight in sync with what's actually on
383
+ * screen, so the rail reads as a set of on/off toggles rather than a set
384
+ * of launch buttons.
385
+ *
386
+ * Driven by widget events rather than by the click handlers above, so the
387
+ * highlight stays correct no matter how a widget opened or closed — rail
388
+ * click, keyboard shortcut, quick-switcher, FAB, or Escape.
389
+ */
390
+ initRailActiveState() {
391
+ const sync = () => this.syncRailActiveState();
392
+
393
+ // Widget lifecycle. 'destroyed' matters because a destroyed widget
394
+ // never emits 'closed'.
395
+ WidgetBus.on('widget:opened', sync);
396
+ WidgetBus.on('widget:closed', sync);
397
+ WidgetBus.on('widget:destroyed', sync);
398
+ // Promotion to a full tab closes the floating window — re-read state.
399
+ WidgetBus.on('widget:open-as-tab', sync);
400
+ // Session switch shows/hides whole sets of widgets via inline display
401
+ // without touching isVisible, which is why syncRailActiveState() tests
402
+ // isShowing() rather than isOpen().
403
+ WidgetBus.on('session:widgets-shown', sync);
404
+
405
+ // Chat search is the one rail entry that isn't a widget, so it has no
406
+ // bus event. Watching the bar's class is cheaper than threading a
407
+ // callback through ChatSearch for a single consumer.
408
+ const searchBar = this.els.chatSearchBar;
409
+ if (searchBar) {
410
+ new MutationObserver(sync).observe(searchBar, {
411
+ attributes: true,
412
+ attributeFilter: ['class'],
413
+ });
414
+ }
415
+
416
+ sync();
417
+ },
418
+
419
+ /**
420
+ * Paint `.active` on every rail button whose target is currently visible.
421
+ * Safe to call at any time — cheap DOM writes, no widget instantiation
422
+ * (isShowing resolves without creating).
423
+ */
424
+ syncRailActiveState() {
425
+ for (const [btnId, widgetId] of Object.entries(RAIL_WIDGET_BUTTONS)) {
426
+ document.getElementById(btnId)
427
+ ?.classList.toggle('active', WidgetManager.isShowing(widgetId));
428
+ }
429
+ document.getElementById('rail-search-btn')
430
+ ?.classList.toggle('active', !!this.chatSearch?.state?.active);
431
+ },
432
+
433
+ /**
434
+ * Resolve the static chrome tooltips declared in web-client.html as
435
+ * data-tooltip-key="<dotted path into strings.yaml>" — the markup carries
436
+ * no user-facing text of its own, so translations stay in one file.
437
+ *
438
+ * Runs before refreshRailShortcutTooltips() so that the shortcut suffix is
439
+ * appended to the resolved string; clearing shortcutBase keeps that safe if
440
+ * the two ever run in the other order.
441
+ */
442
+ applyStaticTooltips() {
443
+ document.querySelectorAll('[data-tooltip-key]').forEach(el => {
444
+ const text = el.dataset.tooltipKey
445
+ .split('.')
446
+ .reduce((o, k) => (o == null ? o : o[k]), S);
447
+ if (typeof text !== 'string') {
448
+ console.warn('Unknown tooltip key:', el.dataset.tooltipKey);
449
+ return;
450
+ }
451
+ el.setAttribute('data-tooltip', withChords(text));
452
+ delete el.dataset.shortcutBase;
453
+ });
454
+ },
455
+
456
+ /**
457
+ * Append the user-configured key to any rail tooltip that opted in via
458
+ * data-shortcut-action="<shortcutId>". Re-run after the shortcut manager
459
+ * reloads user overrides so the tooltip never drifts from the binding.
460
+ */
461
+ refreshRailShortcutTooltips() {
462
+ // Send tooltip first — it doesn't need the manager on keyboard devices.
463
+ this._refreshSendTooltip();
464
+ const sm = this.shortcutManager;
465
+ if (!sm) return;
466
+ document.querySelectorAll('[data-shortcut-action]').forEach(el => {
467
+ const id = el.dataset.shortcutAction;
468
+ const base = el.dataset.shortcutBase ?? (el.dataset.shortcutBase = el.getAttribute('data-tooltip') || '');
469
+ const keys = sm.getShortcutKeys(id);
470
+ const primary = keys?.[0];
471
+ el.setAttribute('data-tooltip', primary ? `${base} (${primary})` : base);
472
+ });
473
+ },
474
+
475
+ /**
476
+ * The send button is NOT a plain shortcut mirror. On keyboard devices plain
477
+ * Enter sends (input-handler.js) and Cmd/Ctrl+Enter is merely the
478
+ * connect-when-disconnected binding — advertising the latter was wrong.
479
+ * On iPhone (no physical keyboard) Enter inserts a newline, so there
480
+ * Cmd+Enter genuinely is the send key.
481
+ */
482
+ _refreshSendTooltip() {
483
+ const btn = this.els?.sendBtn || document.getElementById('send-btn');
484
+ if (!btn) return;
485
+ if (HAS_PHYSICAL_KEYBOARD) {
486
+ btn.setAttribute('data-tooltip', S.ui.input.send_tooltip);
487
+ return;
488
+ }
489
+ const key = this.shortcutManager?.getShortcutKeys('connect')?.[0] || 'Cmd+Enter';
490
+ btn.setAttribute('data-tooltip', S.ui.input.send_tooltip_mod.replace('{key}', key));
491
+ },
492
+
493
+ /**
494
+ * Rail hamburger. Mobile: toggles the slide-in drawer (body.rail-open),
495
+ * scrim/rail-btn taps close it; the composite badge keeps passive awareness
496
+ * of running activity while the drawer is closed. iPad/desktop: toggles the
497
+ * rail between icons-only and icons+labels (body.rail-labels, persisted in
498
+ * user config). The mobile drawer always shows labels.
499
+ */
500
+ initRailDrawer() {
501
+ const toggleBtn = document.getElementById('rail-toggle-btn');
502
+ const scrim = document.getElementById('rail-scrim');
503
+ const compositeBadge = document.getElementById('rail-toggle-badge');
504
+ if (!toggleBtn) return;
505
+
506
+ const isMobile = () => window.matchMedia('(max-width: 600px)').matches;
507
+ const applyRailLabels = () => {
508
+ document.body.classList.toggle('rail-labels', isMobile() || !!configState.config.railExpanded);
509
+ };
510
+ applyRailLabels();
511
+
512
+ const setOpen = (open) => {
513
+ document.body.classList.toggle('rail-open', open);
514
+ toggleBtn.setAttribute('aria-expanded', open ? 'true' : 'false');
515
+ };
516
+ const close = () => setOpen(false);
517
+ const toggle = () => {
518
+ if (isMobile()) {
519
+ setOpen(!document.body.classList.contains('rail-open'));
520
+ } else {
521
+ const expanded = !configState.config.railExpanded;
522
+ configState.setRailExpanded(expanded);
523
+ applyRailLabels();
524
+ toggleBtn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
525
+ }
526
+ };
527
+
528
+ // Exposed as an app method for the toggleRailMenu shortcut (Ctrl/Cmd+B)
529
+ this.toggleRailMenu = toggle;
530
+
531
+ toggleBtn.addEventListener('click', toggle);
532
+ scrim?.addEventListener('click', close);
533
+
534
+ // Auto-close when a rail button is tapped on mobile (after its action runs)
535
+ document.querySelectorAll('#left-rail .rail-btn').forEach(btn => {
536
+ btn.addEventListener('click', () => { if (isMobile()) close(); });
537
+ });
538
+
539
+ // Tabs-overview button in the rail drawer (mobile copy of the one in #tabs).
540
+ // Capture rect first (so dropdown anchors where the user tapped), then
541
+ // close the drawer so the dropdown isn't hidden behind it.
542
+ const railOverviewBtn = document.querySelector('.rail-drawer-grid');
543
+ if (railOverviewBtn) {
544
+ railOverviewBtn.addEventListener('click', (e) => {
545
+ e.stopPropagation();
546
+ const anchorClone = { getBoundingClientRect: () => railOverviewBtn.getBoundingClientRect() };
547
+ if (isMobile()) close();
548
+ this.tabCtrl?.toggleTabsOverview(anchorClone);
549
+ });
550
+ }
551
+
552
+ // Close on viewport resize past the breakpoint so state can't get stuck;
553
+ // re-evaluate labels (drawer forces them on, desktop follows the pref)
554
+ window.addEventListener('resize', () => {
555
+ if (!isMobile()) close();
556
+ applyRailLabels();
557
+ });
558
+
559
+ // Hamburger badge mirrors the active-sessions count. Other rail badges
560
+ // (orphan terminals, agents, tasks, summary forks) intentionally don't contribute
561
+ // — orphans are stale state, not active work.
562
+ if (compositeBadge) {
563
+ const sessionsBadge = document.getElementById('sessions-badge');
564
+ if (sessionsBadge) {
565
+ const update = () => {
566
+ const n = parseInt(sessionsBadge.textContent, 10) || 0;
567
+ compositeBadge.textContent = n > 0 ? String(n) : '';
568
+ };
569
+ new MutationObserver(update).observe(sessionsBadge, { childList: true, characterData: true, subtree: true });
570
+ update();
571
+ }
572
+ }
573
+ },
574
+ };