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,793 @@
1
+ /**
2
+ * Terminal Widget — interactive terminal with xterm.js, mounted via the
3
+ * widget system. Top-sheet on phone/tablet, floating on desktop, and
4
+ * promotable to a tab. Provides a quick-access PTY for the project's
5
+ * CWD, persistent sessions keyed by `(widget session, cwd)`, and a
6
+ * separate per-tab state for the multi-terminal tab strip.
7
+ *
8
+ * This file is the orchestrator: it composes the sub-modules under
9
+ * `widgets/terminal/` and exposes the public surface (`registerTerminalWidget`
10
+ * and the `TerminalWidget` namespace consumed by app.js, tab-controller,
11
+ * orphan-terminals, quick-actions, and the Settings widget). All heavy
12
+ * logic — state, gestures, link providers, init, connection, render,
13
+ * size — lives in dedicated modules so this file stays focused on
14
+ * widget configuration, lifecycle hooks, and the float→tab transfer.
15
+ */
16
+
17
+ import { debug } from '../config.js';
18
+ import { ContextMenu } from '../context-menu.js';
19
+ import S from '../strings.js';
20
+ import { WidgetManager } from '../widget-system/index.js';
21
+
22
+ import {
23
+ destroySessionState,
24
+ fitTerminal,
25
+ getOrCreateTabState,
26
+ getSessionState,
27
+ lastFocusedIfRecent,
28
+ nextTabNumber,
29
+ removeTabState,
30
+ sessionStates,
31
+ tabStates,
32
+ } from './terminal/state.js';
33
+ import {
34
+ copyToClipboard,
35
+ getUnwrappedTerminalSelection,
36
+ } from './terminal/gestures.js';
37
+ import { FilePathLinkProvider, makeLiveCwdRefresher } from './terminal/link-providers.js';
38
+ import {
39
+ connect,
40
+ handleJsonMessage,
41
+ killTerminal,
42
+ updateStatus,
43
+ } from './terminal/connection.js';
44
+ import { initTerminal } from './terminal/init.js';
45
+ import { renderContent } from './terminal/render.js';
46
+ import {
47
+ DEFAULT_TERMINAL_HEIGHT,
48
+ DEFAULT_TERMINAL_WIDTH,
49
+ getConfiguredSize,
50
+ resetConfiguredSize,
51
+ setConfiguredSize,
52
+ } from './terminal/size.js';
53
+
54
+ // ─────────────────────────────────────────────────────────────────────
55
+ // Header context menu (reuses shared ContextMenu from context-menu.js)
56
+ // ─────────────────────────────────────────────────────────────────────
57
+
58
+ let headerContextMenu = null;
59
+
60
+ function showTerminalHeaderMenu(x, y, widget) {
61
+ if (!headerContextMenu) headerContextMenu = new ContextMenu();
62
+
63
+ const dims = widget.getDimensions();
64
+ const configured = getConfiguredSize();
65
+ const isSameSize = dims.width === configured.width && dims.height === configured.height;
66
+
67
+ headerContextMenu.show(x, y, [
68
+ {
69
+ label: `Save current size as default (${dims.width}×${dims.height})`,
70
+ disabled: isSameSize,
71
+ action: () => {
72
+ setConfiguredSize(dims.width, dims.height);
73
+ widget._defaultSize = { width: dims.width, height: dims.height };
74
+ }
75
+ },
76
+ {
77
+ label: `Restore default (${configured.width}×${configured.height})`,
78
+ disabled: isSameSize,
79
+ action: () => {
80
+ widget.size = { ...configured };
81
+ widget.position = {
82
+ x: Math.round((window.innerWidth - configured.width) / 2),
83
+ y: Math.round((window.innerHeight - configured.height) / 2)
84
+ };
85
+ widget.constrainPosition();
86
+ widget.updatePosition();
87
+ widget.updateSize();
88
+ widget.config.onResize?.();
89
+ }
90
+ }
91
+ ]);
92
+ }
93
+
94
+ // ─────────────────────────────────────────────────────────────────────
95
+ // Widget Registration
96
+ // ─────────────────────────────────────────────────────────────────────
97
+
98
+ export function registerTerminalWidget() {
99
+ const configured = getConfiguredSize();
100
+
101
+ WidgetManager.register('terminal', {
102
+ type: 'top-sheet',
103
+ title: S.widgets.titles.terminal,
104
+ icon: 'terminal',
105
+ shortcut: 'Ctrl+`',
106
+
107
+ // Device-specific types
108
+ deviceTypes: {
109
+ default: 'top-sheet',
110
+ phone: 'top-sheet',
111
+ tablet: 'top-sheet',
112
+ desktop: 'floating'
113
+ },
114
+
115
+ // Heights for top-sheet
116
+ heights: {
117
+ half: '30vh',
118
+ full: '50vh'
119
+ },
120
+
121
+ // Floating window size — double-click header to restore to configured size
122
+ size: { width: configured.width, height: configured.height },
123
+ minSize: { width: 400, height: 200 },
124
+
125
+ // Allow transform to these types (including tab via widget system)
126
+ allowedTypes: ['top-sheet', 'floating', 'tab'],
127
+
128
+ // Custom header actions
129
+ headerActions: [
130
+ {
131
+ icon: 'plus',
132
+ title: S.widgets.header_actions.new_terminal_tab,
133
+ onClick: () => window.app?.tabCtrl?.openTerminalWidgetTab()
134
+ },
135
+ {
136
+ icon: 'kill',
137
+ title: S.widgets.header_actions.kill_terminal,
138
+ onClick: () => killTerminal(getSessionState())
139
+ }
140
+ ],
141
+
142
+ // Render function - uses appropriate state based on context
143
+ render: (container, ctx) => {
144
+ let targetState;
145
+
146
+ if (ctx.isTab && ctx.tabId) {
147
+ // Widget tab render - get or create state for this specific tab
148
+ // Pass sessionId from ctx for reconnecting to existing PTY on page reload
149
+ targetState = getOrCreateTabState(ctx.tabId, ctx.cwd, ctx.terminalSessionId);
150
+
151
+ // One-shot command to type after the PTY connects (e.g. `/login` flow).
152
+ // Only set on first render — cleared after send so reconnects don't replay.
153
+ if (ctx.initialCommand && !targetState.terminal) {
154
+ targetState.pendingInitialCommand = ctx.initialCommand;
155
+ }
156
+
157
+ // Check if this is a promotion from floating (transfer flag set)
158
+ const sourceState = getSessionState();
159
+ if (ctx.transferFromFloating && sourceState.terminal && !targetState.terminal) {
160
+ transferFloatingToTab(sourceState, targetState);
161
+ }
162
+ } else {
163
+ // Session (floating) terminal - get per-session state
164
+ targetState = getSessionState(ctx.sessionId);
165
+ }
166
+
167
+ // Update CWD from context
168
+ const newCwd = ctx.cwd || window.app?.activeSession?.cwd;
169
+ if (newCwd !== targetState.cwd) {
170
+ targetState.cwd = newCwd;
171
+ // If terminal exists and CWD changed, reconnect
172
+ if (targetState.initialized && targetState.connected) {
173
+ connect(targetState);
174
+ }
175
+ }
176
+
177
+ renderContent(container, targetState);
178
+
179
+ // Initialize terminal if it doesn't exist yet (new session or new tab)
180
+ // No CWD guard — backend falls back to agents.default_cwd when no CWD is sent
181
+ if (!targetState.terminal) {
182
+ initTerminal(targetState);
183
+ }
184
+
185
+ // Keyboard handlers (Ctrl+C, Cmd+C, Cmd+V) are attached to the Terminal instance
186
+ // via attachCustomKeyEventHandler in initTerminal(), so they survive container re-renders
187
+ },
188
+
189
+ // Open handler - initialize session's terminal
190
+ onOpen: () => {
191
+ const sState = getSessionState();
192
+
193
+ // Get CWD from active session (may be empty on welcome screen — that's OK,
194
+ // backend falls back to agents.default_cwd)
195
+ if (!sState.cwd) {
196
+ sState.cwd = window.app?.activeSession?.cwd || null;
197
+ }
198
+
199
+ // Blur any active terminal tab to release keyboard capture
200
+ if (window.app?.terminalTabManager?.activeTabId) {
201
+ const activeTab = window.app.terminalTabManager.tabs.find(
202
+ t => t.id === window.app.terminalTabManager.activeTabId
203
+ );
204
+ if (activeTab?.terminal) {
205
+ activeTab.terminal.blur();
206
+ }
207
+ }
208
+ // Also blur all widget tab terminals
209
+ for (const [, tabState] of tabStates) {
210
+ if (tabState.terminal) {
211
+ tabState.terminal.blur();
212
+ }
213
+ }
214
+
215
+ // Initialize this session's terminal
216
+ initTerminal(sState);
217
+
218
+ // Ensure widget container has pointer-events (belt and suspenders)
219
+ const widget = WidgetManager.get('terminal');
220
+ if (widget?.container) {
221
+ widget.container.style.pointerEvents = 'auto';
222
+ }
223
+
224
+ // Attach header context menu (once)
225
+ if (widget?.headerEl && !widget.headerEl._termCtxMenu) {
226
+ widget.headerEl._termCtxMenu = true;
227
+ widget.headerEl.addEventListener('contextmenu', (e) => {
228
+ e.preventDefault();
229
+ showTerminalHeaderMenu(e.clientX, e.clientY, widget);
230
+ });
231
+ }
232
+
233
+ // Hide connection bar when terminal is open (saves space, especially on iPad)
234
+ const connBar = document.getElementById('connection-bar');
235
+ if (connBar) {
236
+ connBar.classList.add('hidden-by-terminal');
237
+ }
238
+
239
+ // Focus terminal after animation settles so iOS keyboard
240
+ // viewport shift doesn't race with top-sheet slide-in
241
+ setTimeout(() => {
242
+ if (sState.terminal) {
243
+ sState.terminal.focus();
244
+ }
245
+ }, 350);
246
+ },
247
+
248
+ // Close handler - focus chat input
249
+ onClose: () => {
250
+ // Reset keyboard bar modifiers (unlock Ctrl/Alt, dismiss popups)
251
+ const sState = getSessionState();
252
+ if (sState.keyboardBar) {
253
+ sState.keyboardBar.resetModifiers();
254
+ }
255
+
256
+ // Clear inline pointer-events (set in onOpen) to let CSS handle visibility
257
+ const widget = WidgetManager.get('terminal');
258
+ if (widget?.container) {
259
+ widget.container.style.pointerEvents = '';
260
+ }
261
+
262
+ // Restore connection bar ONLY if no terminal tab is open
263
+ // (when transferring floating→tab, the tab is pushed before close() fires)
264
+ const hasTerminalTab = window.app?.tabCtrl?.widgetTabs?.some(t => t.isTerminal);
265
+ if (!hasTerminalTab) {
266
+ const connBar = document.getElementById('connection-bar');
267
+ if (connBar) {
268
+ connBar.classList.remove('hidden-by-terminal');
269
+ }
270
+ }
271
+
272
+ const chatInput = document.getElementById('message-input');
273
+ if (chatInput) {
274
+ setTimeout(() => chatInput.focus(), 100);
275
+ }
276
+ },
277
+
278
+ // Resize handler - refit session's terminal
279
+ onResize: () => {
280
+ const sState = getSessionState();
281
+ setTimeout(() => fitTerminal(sState), 50);
282
+ },
283
+
284
+ // Transform handler - refit after transform
285
+ onTransform: (fromType, toType) => {
286
+ const sState = getSessionState();
287
+ setTimeout(() => {
288
+ fitTerminal(sState);
289
+ if (sState.terminal) {
290
+ sState.terminal.focus();
291
+ }
292
+ }, 100);
293
+ },
294
+
295
+ // Session change handler - switch terminal to new session's state
296
+ onSessionChange: (sessionId) => {
297
+ const sState = getSessionState(sessionId);
298
+ const newCwd = WidgetManager.currentCwd || window.app?.activeSession?.cwd;
299
+ if (newCwd && newCwd !== sState.cwd) {
300
+ // Clear path cache when CWD changes
301
+ if (sState.filePathProvider) {
302
+ sState.filePathProvider.clearCache();
303
+ }
304
+ sState.cwd = newCwd;
305
+ // Reconnect if widget is visible and terminal is initialized
306
+ const widget = WidgetManager.get('terminal');
307
+ if (widget?.isVisible && sState.initialized) {
308
+ connect(sState);
309
+ }
310
+ }
311
+ },
312
+
313
+ // CWD change handler - update terminal when project directory changes
314
+ onCwdChange: (cwd) => {
315
+ const sState = getSessionState();
316
+ if (cwd && cwd !== sState.cwd) {
317
+ // Clear path cache when CWD changes
318
+ if (sState.filePathProvider) {
319
+ sState.filePathProvider.clearCache();
320
+ }
321
+ sState.cwd = cwd;
322
+ // Reconnect if widget is visible and terminal is initialized
323
+ const widget = WidgetManager.get('terminal');
324
+ if (widget?.isVisible && sState.initialized) {
325
+ connect(sState);
326
+ }
327
+ }
328
+ },
329
+
330
+ // Cleanup when session is closed
331
+ onDestroy: (sessionId) => {
332
+ destroySessionState(sessionId);
333
+ }
334
+ });
335
+ }
336
+
337
+ /**
338
+ * Transfer a live xterm/WebSocket from the session's floating terminal
339
+ * to a tab terminal. This is what powers "Open in Tab" — the user
340
+ * promotes the floating widget without losing scrollback or the running
341
+ * shell. We move the references over, then rebind every handler that
342
+ * captured the source state in its closure (onData/onResize/keyboard
343
+ * handler/WebSocket callbacks) to point at the new target state.
344
+ */
345
+ function transferFloatingToTab(sourceState, targetState) {
346
+ targetState.terminal = sourceState.terminal;
347
+ targetState.fitAddon = sourceState.fitAddon;
348
+ targetState.ws = sourceState.ws;
349
+ targetState.connected = sourceState.connected;
350
+ targetState.initialized = sourceState.initialized;
351
+ targetState.cwd = sourceState.cwd;
352
+ targetState.liveCwd = sourceState.liveCwd;
353
+ targetState.sessionId = sourceState.sessionId;
354
+ targetState.status = sourceState.status;
355
+
356
+ // Create new file path provider for this tab (getCwd closure must reference targetState)
357
+ const newFilePathProvider = new FilePathLinkProvider(
358
+ targetState.terminal,
359
+ () => targetState.liveCwd || targetState.cwd,
360
+ makeLiveCwdRefresher(targetState)
361
+ );
362
+ targetState.terminal.registerLinkProvider(newFilePathProvider);
363
+ targetState.filePathProvider = newFilePathProvider;
364
+
365
+ // Clear source session state (don't dispose - tab owns it now)
366
+ sourceState.terminal = null;
367
+ sourceState.fitAddon = null;
368
+ sourceState.filePathProvider = null;
369
+ sourceState.ws = null;
370
+ sourceState.connected = false;
371
+ sourceState.initialized = false;
372
+ sourceState.sessionId = null;
373
+
374
+ // IMPORTANT: Rebind ALL handlers to use targetState (the new tab).
375
+ // The original handlers captured sourceState in their closure.
376
+ // Old onData/onResize still fire but sourceState.ws is null so they're no-ops.
377
+
378
+ // Rebind terminal input with keyboard bar modifier support
379
+ targetState.terminal.onData(data => {
380
+ if (targetState.ws && targetState.ws.readyState === WebSocket.OPEN) {
381
+ if (targetState.keyboardBar?.hasActiveModifiers()) {
382
+ const mods = targetState.keyboardBar.consumeModifiers();
383
+ let modified = data;
384
+ if (mods.ctrl && data.length === 1) {
385
+ const code = data.toLowerCase().charCodeAt(0);
386
+ if (code >= 97 && code <= 122) {
387
+ modified = String.fromCharCode(code - 96);
388
+ }
389
+ } else if (mods.alt) {
390
+ modified = '\x1b' + data;
391
+ }
392
+ targetState.ws.send(modified);
393
+ } else {
394
+ targetState.ws.send(data);
395
+ }
396
+ }
397
+ });
398
+ targetState.terminal.onResize(({ rows, cols }) => {
399
+ if (targetState.ws && targetState.ws.readyState === WebSocket.OPEN) {
400
+ targetState.ws.send(JSON.stringify({ type: 'resize', rows, cols }));
401
+ }
402
+ });
403
+
404
+ // Rebind attachCustomKeyEventHandler to reference targetState's keyboard bar
405
+ targetState.terminal.attachCustomKeyEventHandler((e) => {
406
+ if (e.type !== 'keydown') return true;
407
+
408
+ // Virtual modifier keys from keyboard bar
409
+ if (targetState.keyboardBar?.hasActiveModifiers()) {
410
+ const ch = e.key;
411
+ if (ch.length === 1) {
412
+ e.preventDefault();
413
+ const mods = targetState.keyboardBar.consumeModifiers();
414
+ let data = ch;
415
+ if (mods.ctrl) {
416
+ const code = ch.toLowerCase().charCodeAt(0);
417
+ if (code >= 97 && code <= 122) {
418
+ data = String.fromCharCode(code - 96);
419
+ }
420
+ }
421
+ if (mods.alt) {
422
+ data = '\x1b' + data;
423
+ }
424
+ if (targetState.ws?.readyState === WebSocket.OPEN) {
425
+ targetState.ws.send(data);
426
+ }
427
+ return false;
428
+ }
429
+ }
430
+
431
+ const key = e.key.toLowerCase();
432
+ if (e.shiftKey || e.altKey) return true;
433
+
434
+ if (key === 'c' && e.metaKey && !e.ctrlKey) {
435
+ const selection = getUnwrappedTerminalSelection(targetState.terminal);
436
+ debug.log('[Terminal/Tab] Cmd+C handler: selection =', JSON.stringify(selection?.substring(0, 80)));
437
+ if (selection) {
438
+ e.preventDefault();
439
+ copyToClipboard(selection);
440
+ }
441
+ return false;
442
+ }
443
+
444
+ if (key === 'c' && e.ctrlKey && !e.metaKey) {
445
+ e.preventDefault();
446
+ if (targetState.ws?.readyState === WebSocket.OPEN) {
447
+ targetState.ws.send('\x03');
448
+ }
449
+ return false;
450
+ }
451
+
452
+ if (key === 'v' && e.metaKey && !e.ctrlKey) {
453
+ e.preventDefault();
454
+ navigator.clipboard.readText().then(text => {
455
+ if (text && targetState.ws?.readyState === WebSocket.OPEN) {
456
+ if (targetState.bracketedPasteMode) {
457
+ targetState.ws.send('\x1b[200~' + text + '\x1b[201~');
458
+ } else {
459
+ targetState.ws.send(text);
460
+ }
461
+ }
462
+ }).catch(() => {});
463
+ return false;
464
+ }
465
+
466
+ // Cmd+N / Ctrl+N → let bubble to shortcut handler (new draft)
467
+ if (key === 'n' && (e.metaKey || e.ctrlKey)) {
468
+ return false;
469
+ }
470
+
471
+ return true;
472
+ });
473
+
474
+ // CRITICAL: Rebind WebSocket handlers to use targetState
475
+ targetState.ws.onmessage = (event) => {
476
+ targetState.lastMessageAt = Date.now();
477
+ const data = event.data;
478
+ if (data.startsWith('{')) {
479
+ try {
480
+ const msg = JSON.parse(data);
481
+ handleJsonMessage(msg, targetState);
482
+ return;
483
+ } catch (e) {}
484
+ }
485
+ if (targetState.terminal) {
486
+ if (targetState.loadingEl) {
487
+ targetState.loadingEl.hidden = true;
488
+ }
489
+ // Track bracketed paste mode from shell output
490
+ if (data.includes('\x1b[?2004h')) targetState.bracketedPasteMode = true;
491
+ if (data.includes('\x1b[?2004l')) targetState.bracketedPasteMode = false;
492
+ targetState.terminal.write(data);
493
+ }
494
+ };
495
+ targetState.ws.onclose = (event) => {
496
+ debug.log('Terminal WebSocket closed:', event.code, event.reason);
497
+ targetState.connected = false;
498
+ updateStatus('disconnected', targetState);
499
+ };
500
+ targetState.ws.onerror = (error) => {
501
+ console.warn('Terminal WebSocket error (connection lost):', error);
502
+ updateStatus('error', targetState);
503
+ };
504
+ }
505
+
506
+ // ─────────────────────────────────────────────────────────────────────
507
+ // Public API
508
+ // ─────────────────────────────────────────────────────────────────────
509
+
510
+ export const TerminalWidget = {
511
+ /**
512
+ * Write text to current session's terminal
513
+ */
514
+ write(text) {
515
+ const sState = getSessionState();
516
+ if (sState.terminal) {
517
+ sState.terminal.write(text);
518
+ }
519
+ },
520
+
521
+ /**
522
+ * Send key to current session's terminal (for keyboard bar)
523
+ */
524
+ sendKey(key, modifiers = {}) {
525
+ const sState = getSessionState();
526
+ if (!sState.ws || sState.ws.readyState !== WebSocket.OPEN) return;
527
+
528
+ const keyMap = {
529
+ 'Escape': '\x1b',
530
+ 'Tab': '\t',
531
+ 'ArrowUp': '\x1b[A',
532
+ 'ArrowDown': '\x1b[B',
533
+ 'ArrowRight': '\x1b[C',
534
+ 'ArrowLeft': '\x1b[D',
535
+ 'Home': '\x1b[H',
536
+ 'End': '\x1b[F',
537
+ 'PageUp': '\x1b[5~',
538
+ 'PageDown': '\x1b[6~',
539
+ 'Delete': '\x1b[3~',
540
+ 'Backspace': '\x7f',
541
+ };
542
+
543
+ let data = keyMap[key] || key;
544
+
545
+ // Apply modifiers
546
+ if (modifiers.ctrl && data.length === 1) {
547
+ const code = data.toLowerCase().charCodeAt(0);
548
+ if (code >= 97 && code <= 122) {
549
+ data = String.fromCharCode(code - 96);
550
+ }
551
+ }
552
+
553
+ sState.ws.send(data);
554
+ },
555
+
556
+ /**
557
+ * Clear current session's terminal
558
+ */
559
+ clear() {
560
+ const sState = getSessionState();
561
+ if (sState.terminal) {
562
+ sState.terminal.clear();
563
+ }
564
+ },
565
+
566
+ /**
567
+ * Focus current session's terminal
568
+ */
569
+ focus() {
570
+ const sState = getSessionState();
571
+ if (sState.terminal) {
572
+ sState.terminal.focus();
573
+ }
574
+ },
575
+
576
+ /**
577
+ * Paste text from clipboard into the active terminal.
578
+ * Returns true if paste was handled, false if no terminal available.
579
+ */
580
+ async paste() {
581
+ // Find the right terminal state: last focused, or active tab, or session floating
582
+ let target = lastFocusedIfRecent();
583
+ if (!target) {
584
+ // Check active widget tab
585
+ const activeTabId = window.app?.tabCtrl?.activeWidgetTabId;
586
+ if (activeTabId) {
587
+ const tabState = tabStates.get(activeTabId);
588
+ if (tabState?.ws?.readyState === WebSocket.OPEN) {
589
+ target = tabState;
590
+ }
591
+ }
592
+ }
593
+ if (!target) {
594
+ // Fall back to session floating terminal
595
+ const sState = getSessionState();
596
+ if (sState.ws?.readyState === WebSocket.OPEN) {
597
+ target = sState;
598
+ }
599
+ }
600
+ if (!target) return false;
601
+
602
+ try {
603
+ const text = await navigator.clipboard.readText();
604
+ if (!text) return false;
605
+ if (target.bracketedPasteMode) {
606
+ target.ws.send('\x1b[200~' + text + '\x1b[201~');
607
+ } else {
608
+ target.ws.send(text);
609
+ }
610
+ target.terminal?.focus();
611
+ return true;
612
+ } catch {
613
+ return false;
614
+ }
615
+ },
616
+
617
+ /**
618
+ * Returns true if a terminal was recently focused (for paste routing)
619
+ */
620
+ isRecentlyFocused() {
621
+ if (lastFocusedIfRecent()) return true;
622
+ // Also check if active tab is a terminal
623
+ const activeTabId = window.app?.tabCtrl?.activeWidgetTabId;
624
+ if (activeTabId) {
625
+ const tab = window.app?.tabCtrl?.widgetTabs?.find(t => t.id === activeTabId);
626
+ if (tab && (tab.widgetId === 'terminal' || tab.isTerminal)) {
627
+ const tabState = tabStates.get(activeTabId);
628
+ if (tabState?.ws?.readyState === WebSocket.OPEN) return true;
629
+ }
630
+ }
631
+ return false;
632
+ },
633
+
634
+ /**
635
+ * Set font size (updates all session terminals and tab terminals)
636
+ */
637
+ setFontSize(fontSize) {
638
+ for (const [, sState] of sessionStates) {
639
+ if (sState.terminal) {
640
+ sState.terminal.options.fontSize = fontSize;
641
+ fitTerminal(sState);
642
+ }
643
+ }
644
+ // Update all tab terminals
645
+ for (const [, tabState] of tabStates) {
646
+ if (tabState.terminal) {
647
+ tabState.terminal.options.fontSize = fontSize;
648
+ fitTerminal(tabState);
649
+ }
650
+ }
651
+ },
652
+
653
+ /**
654
+ * Fit terminal to container
655
+ */
656
+ fit: () => {
657
+ // Fit current session's terminal
658
+ const sState = getSessionState();
659
+ fitTerminal(sState);
660
+ // Fit all tab terminals
661
+ for (const [, tabState] of tabStates) {
662
+ fitTerminal(tabState);
663
+ }
664
+ },
665
+
666
+ /**
667
+ * Get current state (for debugging)
668
+ */
669
+ getState: () => {
670
+ const sessions = {};
671
+ for (const [sid, sState] of sessionStates) {
672
+ sessions[sid] = {
673
+ connected: sState.connected,
674
+ status: sState.status,
675
+ cwd: sState.cwd,
676
+ sessionId: sState.sessionId,
677
+ initialized: sState.initialized,
678
+ };
679
+ }
680
+ const tabs = {};
681
+ for (const [tabId, tabState] of tabStates) {
682
+ tabs[tabId] = {
683
+ connected: tabState.connected,
684
+ status: tabState.status,
685
+ cwd: tabState.cwd,
686
+ sessionId: tabState.sessionId,
687
+ initialized: tabState.initialized,
688
+ };
689
+ }
690
+ return {
691
+ sessions,
692
+ sessionCount: sessionStates.size,
693
+ tabs,
694
+ tabCount: tabStates.size
695
+ };
696
+ },
697
+
698
+ /**
699
+ * Check if current session's terminal is connected
700
+ */
701
+ isConnected: () => getSessionState().connected,
702
+
703
+ /**
704
+ * Get WebSocket (for keyboard bar routing - prefers current session, falls back to first tab with connection)
705
+ */
706
+ getWebSocket: () => {
707
+ const sState = getSessionState();
708
+ if (sState.ws) return sState.ws;
709
+ for (const [, tabState] of tabStates) {
710
+ if (tabState.ws) return tabState.ws;
711
+ }
712
+ return null;
713
+ },
714
+
715
+ /**
716
+ * Get session terminal state (for advanced use)
717
+ * @param {string} [sessionId] - Session ID (defaults to current)
718
+ */
719
+ getSessionTerminalState: (sessionId) => getSessionState(sessionId),
720
+
721
+ /**
722
+ * Get tab terminal state by ID (for advanced use)
723
+ */
724
+ getTabState: (tabId) => tabId ? tabStates.get(tabId) : null,
725
+
726
+ /**
727
+ * Remove and cleanup a tab terminal state
728
+ */
729
+ removeTabState: (tabId) => removeTabState(tabId),
730
+
731
+ /**
732
+ * Get all tab IDs
733
+ */
734
+ getTabIds: () => Array.from(tabStates.keys()),
735
+
736
+ /**
737
+ * Get next terminal tab number for naming (Terminal 1, Terminal 2, etc.)
738
+ */
739
+ getNextTabNumber: () => nextTabNumber(),
740
+
741
+ /**
742
+ * Check if transfer from current session's terminal is available
743
+ */
744
+ hasFloatingTerminal: () => !!getSessionState().terminal,
745
+
746
+ /**
747
+ * Get configured terminal size (for config panel)
748
+ */
749
+ getConfiguredSize,
750
+
751
+ /**
752
+ * Set configured terminal size
753
+ */
754
+ setConfiguredSize,
755
+
756
+ /**
757
+ * Reset configured terminal size to defaults
758
+ */
759
+ resetConfiguredSize,
760
+
761
+ /** Default dimensions */
762
+ DEFAULT_WIDTH: DEFAULT_TERMINAL_WIDTH,
763
+ DEFAULT_HEIGHT: DEFAULT_TERMINAL_HEIGHT,
764
+
765
+ /**
766
+ * Attach terminal to a specific PTY session (e.g., orphaned PTY)
767
+ * Opens the terminal widget and connects with the given sessionId
768
+ * @param {string} ptySessionId - The terminal session ID to attach to
769
+ * @param {string} cwd - Working directory (optional)
770
+ */
771
+ attachToSession(ptySessionId, cwd = null) {
772
+ const sState = getSessionState();
773
+ // Set forced session ID (will be used by connect instead of generating new one)
774
+ sState.forceSessionId = ptySessionId;
775
+ if (cwd) {
776
+ sState.cwd = cwd;
777
+ }
778
+
779
+ // Open the terminal widget using WidgetManager API
780
+ WidgetManager.open('terminal');
781
+
782
+ // If already initialized, reconnect with new session
783
+ // (if not initialized, the render function will call connect with forceSessionId)
784
+ if (sState.initialized) {
785
+ connect(sState);
786
+ }
787
+ },
788
+
789
+ /**
790
+ * Destroy state for a session
791
+ */
792
+ destroySessionState,
793
+ };