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,373 @@
1
+ import S from './strings.js';
2
+ /**
3
+ * Orphaned Terminals Manager
4
+ *
5
+ * Handles detection, display, and reconnection of orphaned terminal sessions
6
+ * (PTY processes running on server without active WebSocket connections)
7
+ */
8
+
9
+ import { CONFIG, debug } from './config.js';
10
+ import { TerminalWidget } from './widget-system/init.js';
11
+ import { escapeHtml, appConfirm } from './utils.js';
12
+ import { showToast } from './context-menu.js';
13
+
14
+ class OrphanTerminalsManager {
15
+ constructor() {
16
+ this.btn = null;
17
+ this.badge = null;
18
+ this.dropdown = null;
19
+ this.list = null;
20
+ this.bulkActions = null;
21
+ this.orphans = [];
22
+ this.isOpen = false;
23
+ }
24
+
25
+ /**
26
+ * Initialize the orphan terminals UI
27
+ */
28
+ init() {
29
+ this.btn = document.getElementById('orphan-terminals-btn');
30
+ this.badge = document.getElementById('orphan-badge');
31
+ this.dropdown = document.getElementById('orphan-dropdown');
32
+ this.list = document.getElementById('orphan-list');
33
+ this.bulkActions = document.getElementById('orphan-bulk-actions');
34
+ const refreshBtn = document.getElementById('orphan-refresh-btn');
35
+ const tabAllBtn = document.getElementById('orphan-tab-all-btn');
36
+ const killAllBtn = document.getElementById('orphan-kill-all-btn');
37
+
38
+ if (!this.btn || !this.dropdown) {
39
+ console.warn('Orphan terminals UI elements not found');
40
+ return;
41
+ }
42
+
43
+ // Toggle dropdown on button click
44
+ this.btn.addEventListener('click', (e) => {
45
+ // Don't toggle if clicking inside dropdown
46
+ if (e.target.closest('.orphan-terminals-dropdown')) {
47
+ return;
48
+ }
49
+ this.toggle();
50
+ });
51
+
52
+ // Refresh button
53
+ if (refreshBtn) {
54
+ refreshBtn.addEventListener('click', (e) => {
55
+ e.stopPropagation();
56
+ this.refresh();
57
+ });
58
+ }
59
+
60
+ // Tab All button
61
+ if (tabAllBtn) {
62
+ tabAllBtn.addEventListener('click', (e) => {
63
+ e.stopPropagation();
64
+ this.tabAll();
65
+ });
66
+ }
67
+
68
+ // Kill All button
69
+ if (killAllBtn) {
70
+ killAllBtn.addEventListener('click', (e) => {
71
+ e.stopPropagation();
72
+ this.killAll();
73
+ });
74
+ }
75
+
76
+ // Close on outside click
77
+ document.addEventListener('click', (e) => {
78
+ if (this.isOpen && !this.btn.contains(e.target)) {
79
+ this.close();
80
+ }
81
+ });
82
+
83
+ // Close on Escape
84
+ document.addEventListener('keydown', (e) => {
85
+ if (e.key === 'Escape' && this.isOpen) {
86
+ this.close();
87
+ }
88
+ });
89
+
90
+ // Initial check
91
+ this.refresh();
92
+
93
+ // Periodic refresh every 30 seconds
94
+ setInterval(() => this.refreshBadge(), 30000);
95
+ }
96
+
97
+ /**
98
+ * Toggle dropdown visibility
99
+ */
100
+ toggle() {
101
+ if (this.isOpen) {
102
+ this.close();
103
+ } else {
104
+ this.open();
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Open the dropdown
110
+ */
111
+ open() {
112
+ this.isOpen = true;
113
+ this.dropdown.classList.add('visible');
114
+ this.refresh();
115
+ }
116
+
117
+ /**
118
+ * Close the dropdown
119
+ */
120
+ close() {
121
+ this.isOpen = false;
122
+ this.dropdown.classList.remove('visible');
123
+ }
124
+
125
+ /**
126
+ * Refresh the orphaned terminals list
127
+ */
128
+ async refresh() {
129
+ try {
130
+ const orphans = await this.fetchOrphans();
131
+ this.orphans = orphans;
132
+ this.updateBadge();
133
+ this.renderList();
134
+ } catch (err) {
135
+ console.error('Failed to fetch orphaned terminals:', err);
136
+ this.list.innerHTML = `<div class="orphan-dropdown-empty">${S.orphan_terminals.failed_load}</div>`;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Just refresh the badge count (lightweight)
142
+ */
143
+ async refreshBadge() {
144
+ try {
145
+ const orphans = await this.fetchOrphans();
146
+ this.orphans = orphans;
147
+ this.updateBadge();
148
+ } catch (err) {
149
+ // Silently fail for background refresh
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Fetch orphaned terminals from server
155
+ * @returns {Promise<Array>} List of orphaned terminal sessions
156
+ */
157
+ async fetchOrphans() {
158
+ const response = await fetch(`${CONFIG.API_BASE}/api/terminals`);
159
+ if (!response.ok) {
160
+ throw new Error(`HTTP ${response.status}`);
161
+ }
162
+ const data = await response.json();
163
+ const serverTerminals = data.terminals || [];
164
+
165
+ // Get currently connected terminal session IDs
166
+ const connectedIds = this.getConnectedSessionIds();
167
+
168
+ // Filter to only orphaned ones (not connected by any tab)
169
+ return serverTerminals.filter(t => !connectedIds.has(t.session));
170
+ }
171
+
172
+ /**
173
+ * Get session IDs of all currently connected terminals
174
+ * @returns {Set<string>}
175
+ */
176
+ getConnectedSessionIds() {
177
+ const ids = new Set();
178
+
179
+ // Get tab terminal session IDs
180
+ const tabIds = TerminalWidget.getTabIds();
181
+ for (const tabId of tabIds) {
182
+ const state = TerminalWidget.getTabState(tabId);
183
+ if (state?.sessionId) {
184
+ ids.add(state.sessionId);
185
+ }
186
+ }
187
+
188
+ // Get floating terminal session ID if exists
189
+ // Note: Floating terminal uses a different state access pattern
190
+ // We'll check via the floating state export if available
191
+
192
+ return ids;
193
+ }
194
+
195
+ /**
196
+ * Update the badge count and bulk actions visibility
197
+ */
198
+ updateBadge() {
199
+ const count = this.orphans.filter(t => t.alive).length;
200
+ if (this.badge) {
201
+ this.badge.textContent = count > 0 ? count : '';
202
+ this.badge.dataset.count = count;
203
+ }
204
+ // Show bulk actions only when there are orphans
205
+ if (this.bulkActions) {
206
+ this.bulkActions.style.display = this.orphans.length > 0 ? 'flex' : 'none';
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Render the orphaned terminals list
212
+ */
213
+ renderList() {
214
+ if (!this.list) return;
215
+
216
+ if (this.orphans.length === 0) {
217
+ this.list.innerHTML = `<div class="orphan-dropdown-empty">${S.orphan_terminals.empty}</div>`;
218
+ return;
219
+ }
220
+
221
+ this.list.innerHTML = this.orphans.map(term => `
222
+ <div class="orphan-terminal-item" data-session="${escapeHtml(term.session)}">
223
+ <div class="orphan-terminal-info">
224
+ <span class="orphan-terminal-pid">PID ${term.pid || '?'}</span>
225
+ <span class="orphan-terminal-status ${term.alive ? '' : 'dead'}">
226
+ ${term.alive ? 'running' : 'dead'}
227
+ </span>
228
+ </div>
229
+ <div class="orphan-terminal-cwd" data-tooltip="${escapeHtml(term.cwd || '')}">${escapeHtml(term.cwd || 'unknown')}</div>
230
+ <div class="orphan-terminal-actions">
231
+ ${term.alive ? `
232
+ <button class="attach-btn" data-action="tab">Tab</button>
233
+ <button class="float-btn" data-action="float">Float</button>
234
+ ` : ''}
235
+ <button class="kill-btn" data-action="kill">Kill</button>
236
+ </div>
237
+ </div>
238
+ `).join('');
239
+
240
+ // Add click handlers
241
+ this.list.querySelectorAll('.orphan-terminal-item').forEach(item => {
242
+ item.querySelectorAll('button').forEach(btn => {
243
+ btn.addEventListener('click', (e) => {
244
+ e.stopPropagation();
245
+ const action = btn.dataset.action;
246
+ const session = item.dataset.session;
247
+ if (action === 'tab') {
248
+ this.attachToTab(session);
249
+ } else if (action === 'float') {
250
+ this.attachToFloat(session);
251
+ } else if (action === 'kill') {
252
+ this.killTerminal(session);
253
+ }
254
+ });
255
+ });
256
+ });
257
+ }
258
+
259
+ /**
260
+ * Attach to an orphaned terminal by creating a new tab with its session ID
261
+ * @param {string} sessionId - The terminal session ID to attach to
262
+ */
263
+ attachToTab(sessionId) {
264
+ const term = this.orphans.find(t => t.session === sessionId);
265
+ if (!term) return;
266
+
267
+ // Create a new terminal tab with this session ID
268
+ if (window.app?.tabCtrl) {
269
+ const tabId = window.app.tabCtrl.openTerminalWidgetTab({
270
+ terminalSessionId: sessionId,
271
+ cwd: term.cwd
272
+ });
273
+ debug.log(`Attached to orphaned terminal ${sessionId} as tab ${tabId}`);
274
+ }
275
+
276
+ this.close();
277
+ this.refresh();
278
+ }
279
+
280
+ /**
281
+ * Attach to an orphaned terminal in the floating panel
282
+ * @param {string} sessionId - The terminal session ID to attach to
283
+ */
284
+ attachToFloat(sessionId) {
285
+ const term = this.orphans.find(t => t.session === sessionId);
286
+ if (!term) return;
287
+
288
+ // Attach to floating terminal with this session ID
289
+ TerminalWidget.attachToSession(sessionId, term.cwd);
290
+ debug.log(`Attached to orphaned terminal ${sessionId} in floating panel`);
291
+
292
+ this.close();
293
+ this.refresh();
294
+ }
295
+
296
+ /**
297
+ * Kill an orphaned terminal
298
+ * @param {string} sessionId - The terminal session ID to kill
299
+ */
300
+ async killTerminal(sessionId) {
301
+ try {
302
+ const response = await fetch(`${CONFIG.API_BASE}/api/terminal/${encodeURIComponent(sessionId)}`, {
303
+ method: 'DELETE'
304
+ });
305
+
306
+ if (!response.ok) {
307
+ throw new Error(`HTTP ${response.status}`);
308
+ }
309
+
310
+ debug.log(`Killed orphaned terminal: ${sessionId}`);
311
+ this.refresh();
312
+ } catch (err) {
313
+ console.error('Failed to kill terminal:', err);
314
+ alert(S.orphan_terminals.kill_failed.replace('{error}', err.message));
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Open all alive orphaned terminals in tabs
320
+ */
321
+ tabAll() {
322
+ const aliveOrphans = this.orphans.filter(t => t.alive);
323
+ if (aliveOrphans.length === 0) return;
324
+
325
+ for (const term of aliveOrphans) {
326
+ if (window.app?.tabCtrl) {
327
+ window.app.tabCtrl.openTerminalWidgetTab({
328
+ terminalSessionId: term.session,
329
+ cwd: term.cwd
330
+ });
331
+ }
332
+ }
333
+
334
+ debug.log(`Opened ${aliveOrphans.length} orphaned terminals as tabs`);
335
+ this.close();
336
+ this.refresh();
337
+ }
338
+
339
+ /**
340
+ * Kill all orphaned terminals
341
+ */
342
+ async killAll() {
343
+ if (this.orphans.length === 0) return;
344
+
345
+ const count = this.orphans.length;
346
+ const ok = await appConfirm(
347
+ S.orphan_terminals.kill_all_confirm.replace('{count}', count),
348
+ { confirmLabel: 'Kill All', danger: true }
349
+ );
350
+ if (!ok) return;
351
+
352
+ const results = await Promise.allSettled(
353
+ this.orphans.map(term =>
354
+ fetch(`${CONFIG.API_BASE}/api/terminal/${encodeURIComponent(term.session)}`, {
355
+ method: 'DELETE'
356
+ })
357
+ )
358
+ );
359
+
360
+ const succeeded = results.filter(r => r.status === 'fulfilled' && r.value.ok).length;
361
+ const failed = count - succeeded;
362
+
363
+ debug.log(`Killed ${succeeded} orphaned terminals${failed > 0 ? `, ${failed} failed` : ''}`);
364
+ showToast(failed > 0
365
+ ? S.orphan_terminals.kill_all_partial
366
+ .replace('{ok}', succeeded).replace('{count}', count).replace('{failed}', failed)
367
+ : S.orphan_terminals.kill_all_done.replace('{count}', succeeded));
368
+ this.refresh();
369
+ }
370
+ }
371
+
372
+ // Singleton instance
373
+ export const orphanTerminals = new OrphanTerminalsManager();
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Server-flavored path helpers.
3
+ *
4
+ * Every path the client manipulates belongs to the SERVER's filesystem,
5
+ * not the browser's — an iPad can be driving a Windows server — so the
6
+ * flavor comes from what the server reports (INSTANCE_CONFIG.path_style),
7
+ * never from navigator.platform.
8
+ *
9
+ * Before this module the resolver was POSIX-only: `startsWith('/')` meant
10
+ * "absolute" and joins were `'/' + parts.join('/')`, so a Windows cwd came
11
+ * back as `/C:/Users/me/proj` — a drive letter demoted to a path segment,
12
+ * which then 404s every listing.
13
+ */
14
+
15
+ const WIN_ABS = /^[A-Za-z]:[\\/]/;
16
+ const UNC = /^\\\\[^\\/]+[\\/][^\\/]+/;
17
+
18
+ /** Windows path semantics? Falls back to sniffing a known server path so a
19
+ * cached page that predates the config flag still behaves. */
20
+ export function isWindowsPaths(sampleAbsPath = '') {
21
+ const declared = window.INSTANCE_CONFIG?.path_style;
22
+ if (declared === 'windows') return true;
23
+ if (declared === 'posix') return false;
24
+ return WIN_ABS.test(sampleAbsPath) || UNC.test(sampleAbsPath);
25
+ }
26
+
27
+ export function pathSep(sample = '') {
28
+ return isWindowsPaths(sample) ? '\\' : '/';
29
+ }
30
+
31
+ /**
32
+ * Which separators the flavor ACCEPTS — a different question from which one
33
+ * `pathSep` emits, and the one this module used to get wrong. Windows treats
34
+ * '\' and '/' interchangeably, but on POSIX a backslash is an ordinary
35
+ * filename character: splitting on it truncates a real file called
36
+ * `notes\2024-Q1.md` down to `2024-Q1.md`.
37
+ */
38
+ function splitRe(sample) { return isWindowsPaths(sample) ? /[\\/]+/ : /\/+/; }
39
+ function trailSepRe(sample) { return isWindowsPaths(sample) ? /[\\/]+$/ : /\/+$/; }
40
+ function leadSepRe(sample) { return isWindowsPaths(sample) ? /^[\\/]+/ : /^\/+/; }
41
+
42
+ /** Windows flavor for a pair of paths — either one can carry the tell (a
43
+ * drive letter or UNC prefix), and a declared path_style outranks both. */
44
+ function winPair(a, b) { return isWindowsPaths(a || '') || isWindowsPaths(b || ''); }
45
+
46
+ /** Does this path stand on its own (no cwd needed)? */
47
+ export function isAbsolutePath(p, sample = '') {
48
+ if (!p) return false;
49
+ if (isWindowsPaths(sample || p)) return WIN_ABS.test(p) || UNC.test(p);
50
+ return p.startsWith('/');
51
+ }
52
+
53
+ /**
54
+ * The leading part that must survive normalization: '/' on POSIX,
55
+ * 'C:\' or '\\server\share\' on Windows. '' for a relative path.
56
+ */
57
+ export function pathRoot(p) {
58
+ if (!p) return '';
59
+ if (isWindowsPaths(p)) {
60
+ const unc = p.match(UNC);
61
+ if (unc) return unc[0] + '\\';
62
+ if (WIN_ABS.test(p)) return p.slice(0, 2) + '\\';
63
+ }
64
+ if (p.startsWith('/')) return '/';
65
+ return '';
66
+ }
67
+
68
+ /**
69
+ * Split into components, dropping empties. `sample` exists for callers that
70
+ * hand us a rootless fragment — the drive letter that makes a path
71
+ * recognizably Windows has already been sliced off, so the fragment alone
72
+ * would sniff as POSIX.
73
+ */
74
+ export function splitPath(p, sample = p) {
75
+ return (p || '').split(splitRe(sample)).filter(Boolean);
76
+ }
77
+
78
+ /**
79
+ * Resolve `rel` against `base`, collapsing . and .. — the cross-platform
80
+ * replacement for `'/' + combined.split('/').filter(...).join('/')`.
81
+ */
82
+ export function resolvePath(base, rel) {
83
+ const startFrom = isAbsolutePath(rel, base) ? rel : `${base || ''}/${rel || ''}`;
84
+ const root = pathRoot(startFrom);
85
+ const sep = isWindowsPaths(startFrom) ? '\\' : '/';
86
+ const out = [];
87
+ for (const part of splitPath(root ? startFrom.slice(root.length) : startFrom, startFrom)) {
88
+ if (part === '.') continue;
89
+ if (part === '..') out.pop();
90
+ else out.push(part);
91
+ }
92
+ if (!root) return out.join(sep);
93
+ // root already ends with a separator; don't double it
94
+ return root + out.join(sep);
95
+ }
96
+
97
+ /** Last component, either separator. */
98
+ export function basename(p) {
99
+ const parts = splitPath(p);
100
+ return parts.length ? parts[parts.length - 1] : '';
101
+ }
102
+
103
+ /** Containing directory, or null at the root. */
104
+ export function parentOf(p) {
105
+ if (!p) return null;
106
+ const root = pathRoot(p);
107
+ const parts = splitPath(root ? p.slice(root.length) : p, p);
108
+ if (!parts.length) return null; // already at the root
109
+ parts.pop();
110
+ const sep = isWindowsPaths(p) ? '\\' : '/';
111
+ if (root) return parts.length ? root + parts.join(sep) : root;
112
+ return parts.join(sep) || null;
113
+ }
114
+
115
+ /** Join a directory and a child name with the server's separator. */
116
+ export function joinPath(dir, child) {
117
+ if (!dir) return child || '';
118
+ if (!child) return dir;
119
+ const sep = isWindowsPaths(dir) ? '\\' : '/';
120
+ return dir.replace(trailSepRe(dir), '') + sep + child;
121
+ }
122
+
123
+ /** Trailing separators removed, but never past the root. */
124
+ export function stripTrailingSep(p) {
125
+ if (!p) return p;
126
+ const root = pathRoot(p);
127
+ const trimmed = p.replace(trailSepRe(p), '');
128
+ return trimmed.length >= root.length && trimmed ? trimmed : (root || p);
129
+ }
130
+
131
+ /**
132
+ * `path` expressed relative to `base`, or `path` unchanged when it isn't
133
+ * under `base`. Replaces the `p.startsWith(cwd + '/') ? p.slice(cwd.length + 1) : p`
134
+ * idiom, which on Windows never matched — so shadow-git/history endpoints
135
+ * that require a repo-relative pathspec were handed absolute paths and
136
+ * silently returned nothing.
137
+ *
138
+ * Emits forward slashes on Windows: the consumers are git pathspecs and API
139
+ * keys, which are '/' on every platform. On POSIX the backslashes that
140
+ * survive are part of the filename, so they're left alone.
141
+ */
142
+ export function relativeTo(path, base) {
143
+ if (!path || !base) return path;
144
+ if (!isUnder(path, base)) return path;
145
+ const win = winPair(path, base);
146
+ // stripTrailingSep is what isUnder compared against — including when
147
+ // base IS the root, where it stops at '/' or 'C:\' rather than emptying
148
+ // out. Slicing by any other length eats a real character.
149
+ const rest = path.slice(stripTrailingSep(base).length);
150
+ const trimmed = rest.replace(leadSepRe(base), '');
151
+ return win ? trimmed.replace(/\\/g, '/') : trimmed;
152
+ }
153
+
154
+ /** Directory part of a path — '' when there's no separator. */
155
+ export function dirname(p) {
156
+ const parent = parentOf(p);
157
+ return parent === null ? '' : parent;
158
+ }
159
+
160
+ /**
161
+ * Is `child` inside `dir` (or equal to it)?
162
+ *
163
+ * On Windows this is case- and separator-insensitive, because the same file
164
+ * is reachable as `C:/Users/me/proj` or `C:\Users\me\proj` and the two sides
165
+ * often come from different places (the server reports one, a link the other).
166
+ * On POSIX neither folding applies — case matters and '\' is a filename
167
+ * character. Normalization is 1:1 in length apart from the trailing strip,
168
+ * so `relativeTo` can slice by `stripTrailingSep(dir).length`.
169
+ */
170
+ export function isUnder(child, dir) {
171
+ if (!child || !dir) return false;
172
+ const win = winPair(child, dir);
173
+ const sep = win ? '\\' : '/';
174
+ const norm = (s) => (win ? s.toLowerCase().replace(/\//g, '\\') : s).replace(win ? /\\+$/ : /\/+$/, '');
175
+ const c = norm(child);
176
+ const d = norm(dir);
177
+ if (c === d) return true;
178
+ return c.startsWith(d) && c.charAt(d.length) === sep;
179
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Performance instrumentation module.
3
+ * Only loaded when ?perf=true is in the URL.
4
+ * Wraps key functions with performance.mark/measure calls.
5
+ *
6
+ * Usage from Playwright:
7
+ * window.__perfCollect() → returns all measures as array
8
+ * window.__perfClear() → clears all marks/measures
9
+ * window.__perfEnabled → true when loaded
10
+ */
11
+
12
+ import { debug } from './config.js';
13
+
14
+ window.__perfEnabled = true;
15
+
16
+ // ── Wrap a method with performance marks ──
17
+ function wrapMethod(obj, methodName, label) {
18
+ const original = obj[methodName];
19
+ if (!original) return;
20
+ obj[methodName] = function (...args) {
21
+ const markStart = `${label}-start`;
22
+ const markEnd = `${label}-end`;
23
+ performance.mark(markStart);
24
+ const result = original.apply(this, args);
25
+ performance.mark(markEnd);
26
+ performance.measure(label, markStart, markEnd);
27
+ return result;
28
+ };
29
+ }
30
+
31
+ // ── Instrument ChatController.renderMessages ──
32
+ function instrumentChatController() {
33
+ const chatCtrl = window.app?.chatCtrl;
34
+ if (!chatCtrl) return false;
35
+ wrapMethod(chatCtrl, 'renderMessages', 'render-messages');
36
+ wrapMethod(chatCtrl, '_renderMessagesIntoContainer', 'render-into-container');
37
+ return true;
38
+ }
39
+
40
+ // ── Instrument SessionContainerPool.activate ──
41
+ function instrumentContainerPool() {
42
+ const pool = window.app?.chatCtrl?._containerPool;
43
+ if (!pool) return false;
44
+ wrapMethod(pool, 'activate', 'pool-activate');
45
+ return true;
46
+ }
47
+
48
+ // ── Instrument App._doSessionSwitch ──
49
+ function instrumentSessionSwitch() {
50
+ const app = window.app;
51
+ if (!app) return false;
52
+ wrapMethod(app, '_doSessionSwitch', 'session-switch');
53
+ return true;
54
+ }
55
+
56
+ // ── API: Collect all measures ──
57
+ window.__perfCollect = function () {
58
+ return performance.getEntriesByType('measure').map(e => ({
59
+ name: e.name,
60
+ duration: Math.round(e.duration * 100) / 100,
61
+ startTime: Math.round(e.startTime * 100) / 100,
62
+ }));
63
+ };
64
+
65
+ // ── API: Clear all marks and measures ──
66
+ window.__perfClear = function () {
67
+ performance.clearMarks();
68
+ performance.clearMeasures();
69
+ };
70
+
71
+ // ── API: Get measures by name ──
72
+ window.__perfGet = function (name) {
73
+ return performance.getEntriesByName(name, 'measure').map(e => ({
74
+ duration: Math.round(e.duration * 100) / 100,
75
+ startTime: Math.round(e.startTime * 100) / 100,
76
+ }));
77
+ };
78
+
79
+ // ── Apply all instrumentation ──
80
+ function applyInstrumentation() {
81
+ const results = {
82
+ chatController: instrumentChatController(),
83
+ containerPool: instrumentContainerPool(),
84
+ sessionSwitch: instrumentSessionSwitch(),
85
+ };
86
+ debug.log('[Perf] Instrumentation applied:', results);
87
+ }
88
+
89
+ // App may not be ready yet, wait for it
90
+ if (window.app?.chatCtrl) {
91
+ applyInstrumentation();
92
+ } else {
93
+ // Poll briefly for app readiness
94
+ let attempts = 0;
95
+ const interval = setInterval(() => {
96
+ if (window.app?.chatCtrl || ++attempts > 50) {
97
+ clearInterval(interval);
98
+ if (window.app?.chatCtrl) applyInstrumentation();
99
+ else console.warn('[Perf] App not ready after 5s, skipping instrumentation');
100
+ }
101
+ }, 100);
102
+ }