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,794 @@
1
+ /**
2
+ * Git Widget - Repository status and history viewer using the widget system
3
+ *
4
+ * Shows uncommitted changes, commit history, and file diffs.
5
+ * Migrated from git-panel.js to use the modular widget system.
6
+ *
7
+ * Views: status → diff → history → commit detail
8
+ */
9
+
10
+ import S from '../strings.js';
11
+ import { escapeHtml, formatRelativeTime } from '../utils.js';
12
+ import { CONFIG } from '../config.js';
13
+ import { WidgetManager, WidgetBus, ICONS } from '../widget-system/index.js';
14
+ import { basename } from '../path-utils.js';
15
+
16
+ // File status icons with colors
17
+ const STATUS_ICONS = {
18
+ modified: { icon: 'M', color: 'var(--accent-orange)', label: S.widgets.git.statuses.modified },
19
+ added: { icon: 'A', color: 'var(--accent-green)', label: S.widgets.git.statuses.added },
20
+ deleted: { icon: 'D', color: 'var(--accent-red)', label: S.widgets.git.statuses.deleted },
21
+ renamed: { icon: 'R', color: 'var(--accent-blue)', label: S.widgets.git.statuses.renamed },
22
+ copied: { icon: 'C', color: 'var(--accent-purple)', label: S.widgets.git.statuses.copied },
23
+ untracked: { icon: '?', color: 'var(--text-muted)', label: S.widgets.git.statuses.untracked },
24
+ };
25
+
26
+ /**
27
+ * Git Widget State
28
+ * Kept separate from widget so it persists across transforms
29
+ */
30
+ class GitState {
31
+ constructor() {
32
+ this.cwd = null;
33
+ this.gitStatus = null;
34
+ this.commits = [];
35
+ this.hasMoreCommits = false;
36
+ this.view = 'status'; // 'status', 'diff', 'history', 'commit'
37
+ this.selectedFile = null;
38
+ this.selectedFileStaged = false;
39
+ this.selectedCommit = null;
40
+ this.loading = false;
41
+ this.error = null;
42
+ this.currentContainer = null; // For tab mode rendering
43
+ }
44
+
45
+ reset() {
46
+ this.gitStatus = null;
47
+ this.commits = [];
48
+ this.hasMoreCommits = false;
49
+ this.view = 'status';
50
+ this.selectedFile = null;
51
+ this.selectedFileStaged = false;
52
+ this.selectedCommit = null;
53
+ this.loading = false;
54
+ this.error = null;
55
+ }
56
+ }
57
+
58
+ // Per-session state map
59
+ const states = new Map();
60
+
61
+ function getState(sessionId) {
62
+ if (!sessionId) sessionId = WidgetManager.currentSessionId;
63
+ if (!states.has(sessionId)) states.set(sessionId, new GitState());
64
+ return states.get(sessionId);
65
+ }
66
+
67
+ function destroyState(sessionId) {
68
+ states.delete(sessionId);
69
+ }
70
+
71
+ // ─────────────────────────────────────────────────────────────────────
72
+ // Data Loading
73
+ // ─────────────────────────────────────────────────────────────────────
74
+
75
+ async function loadStatus() {
76
+ const state = getState();
77
+ if (!state.cwd) return;
78
+
79
+ state.loading = true;
80
+ state.error = null;
81
+ renderContent();
82
+
83
+ try {
84
+ const url = `${CONFIG.API_BASE}/api/git/status?cwd=${encodeURIComponent(state.cwd)}`;
85
+ const response = await fetch(url);
86
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
87
+
88
+ const data = await response.json();
89
+
90
+ if (data.error === 'not_a_repo') {
91
+ state.loading = false;
92
+ state.error = 'Not a git repository';
93
+ renderContent();
94
+ return;
95
+ }
96
+
97
+ state.gitStatus = data;
98
+ state.loading = false;
99
+ state.view = 'status';
100
+ updateSummary();
101
+ renderContent();
102
+
103
+ // Update status bar branch (git panel has fresher data than session switch)
104
+ if (data.branch && window.app?.statusBar) {
105
+ window.app.statusBar.updateBranch(data.branch);
106
+ }
107
+
108
+ } catch (err) {
109
+ state.loading = false;
110
+ state.error = `Failed to load git status: ${err.message}`;
111
+ renderContent();
112
+ }
113
+ }
114
+
115
+ async function loadDiff(file = null, staged = false) {
116
+ const state = getState();
117
+ if (!state.cwd) return null;
118
+
119
+ try {
120
+ let url = `${CONFIG.API_BASE}/api/git/diff?cwd=${encodeURIComponent(state.cwd)}`;
121
+ if (file) url += `&file=${encodeURIComponent(file)}`;
122
+ if (staged) url += '&staged=true';
123
+
124
+ const response = await fetch(url);
125
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
126
+
127
+ return await response.json();
128
+ } catch (err) {
129
+ console.error('Failed to load diff:', err);
130
+ return null;
131
+ }
132
+ }
133
+
134
+ async function loadHistory(skip = 0) {
135
+ const state = getState();
136
+ if (!state.cwd) return;
137
+
138
+ if (skip === 0) {
139
+ state.loading = true;
140
+ state.commits = [];
141
+ renderContent();
142
+ }
143
+
144
+ try {
145
+ const url = `${CONFIG.API_BASE}/api/git/log?cwd=${encodeURIComponent(state.cwd)}&limit=20&skip=${skip}`;
146
+ const response = await fetch(url);
147
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
148
+
149
+ const data = await response.json();
150
+ state.commits = [...state.commits, ...data.commits];
151
+ state.hasMoreCommits = data.hasMore;
152
+ state.loading = false;
153
+ renderContent();
154
+
155
+ } catch (err) {
156
+ state.loading = false;
157
+ state.error = `Failed to load history: ${err.message}`;
158
+ renderContent();
159
+ }
160
+ }
161
+
162
+ async function loadCommit(hash) {
163
+ const state = getState();
164
+ if (!state.cwd) return null;
165
+
166
+ try {
167
+ const url = `${CONFIG.API_BASE}/api/git/show/${hash}?cwd=${encodeURIComponent(state.cwd)}`;
168
+ const response = await fetch(url);
169
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
170
+
171
+ return await response.json();
172
+ } catch (err) {
173
+ console.error('Failed to load commit:', err);
174
+ return null;
175
+ }
176
+ }
177
+
178
+ function refresh() {
179
+ const state = getState();
180
+ if (state.view === 'history') {
181
+ loadHistory(0);
182
+ } else if (state.view === 'commit' && state.selectedCommit) {
183
+ showCommitDetail(state.selectedCommit);
184
+ } else {
185
+ loadStatus();
186
+ }
187
+ }
188
+
189
+ // ─────────────────────────────────────────────────────────────────────
190
+ // Summary Update
191
+ // ─────────────────────────────────────────────────────────────────────
192
+
193
+ function updateSummary() {
194
+ const state = getState();
195
+ const widget = WidgetManager.get('git');
196
+ if (!widget || !state.gitStatus) return;
197
+
198
+ const { branch, ahead, behind, summary } = state.gitStatus;
199
+
200
+ // Build HTML summary with branch pill and colored count badges
201
+ let html = `<span class="git-summary-branch">${escapeHtml(branch || '?')}</span>`;
202
+
203
+ // Ahead/behind indicators
204
+ if (ahead > 0) html += `<span class="git-summary-sync ahead">↑${ahead}</span>`;
205
+ if (behind > 0) html += `<span class="git-summary-sync behind">↓${behind}</span>`;
206
+
207
+ const total = summary.stagedCount + summary.modifiedCount + summary.untrackedCount;
208
+ if (total === 0) {
209
+ html += `<span class="git-summary-badge clean" data-tooltip="Working tree clean">✓</span>`;
210
+ } else {
211
+ // Compact colored badges
212
+ if (summary.stagedCount > 0) {
213
+ html += `<span class="git-summary-badge staged" data-tooltip="${summary.stagedCount} staged">${summary.stagedCount}</span>`;
214
+ }
215
+ if (summary.modifiedCount > 0) {
216
+ html += `<span class="git-summary-badge modified" data-tooltip="${summary.modifiedCount} modified">${summary.modifiedCount}</span>`;
217
+ }
218
+ if (summary.untrackedCount > 0) {
219
+ html += `<span class="git-summary-badge untracked" data-tooltip="${summary.untrackedCount} untracked">${summary.untrackedCount}</span>`;
220
+ }
221
+ }
222
+
223
+ widget.setSummaryHTML(html);
224
+ }
225
+
226
+ // ─────────────────────────────────────────────────────────────────────
227
+ // Rendering
228
+ // ─────────────────────────────────────────────────────────────────────
229
+
230
+ function renderContent() {
231
+ const state = getState();
232
+ // Get container: either from visible widget or from state (tab mode)
233
+ const widget = WidgetManager.get('git');
234
+ const container = (widget?.isVisible ? widget.contentContainer : null) || state.currentContainer;
235
+ if (!container) return;
236
+
237
+ // Loading state
238
+ if (state.loading) {
239
+ container.innerHTML = '<div class="git-loading">Loading...</div>';
240
+ return;
241
+ }
242
+
243
+ // Error state
244
+ if (state.error) {
245
+ container.innerHTML = `<div class="git-error">${escapeHtml(state.error)}</div>`;
246
+ return;
247
+ }
248
+
249
+ // Route to appropriate view
250
+ switch (state.view) {
251
+ case 'diff':
252
+ // Already rendered by showFileDiff
253
+ break;
254
+ case 'history':
255
+ renderHistoryView(container);
256
+ break;
257
+ case 'commit':
258
+ // Already rendered by showCommitDetail
259
+ break;
260
+ default:
261
+ renderStatusView(container);
262
+ }
263
+ }
264
+
265
+ // ─────────────────────────────────────────────────────────────────────
266
+ // Status View
267
+ // ─────────────────────────────────────────────────────────────────────
268
+
269
+ function renderStatusView(container) {
270
+ const state = getState();
271
+ if (!state.gitStatus) {
272
+ container.innerHTML = '<div class="git-empty">No git repository</div>';
273
+ return;
274
+ }
275
+
276
+ const { staged, modified, untracked } = state.gitStatus;
277
+ const total = staged.length + modified.length + untracked.length;
278
+
279
+ if (total === 0) {
280
+ container.innerHTML = '<div class="git-empty">Working tree clean</div>';
281
+ return;
282
+ }
283
+
284
+ let html = '<div class="git-content">';
285
+
286
+ if (staged.length > 0) {
287
+ html += renderFileSection('Staged Changes', staged, true, 'staged');
288
+ }
289
+
290
+ if (modified.length > 0) {
291
+ html += renderFileSection('Changes', modified, false, 'modified');
292
+ }
293
+
294
+ if (untracked.length > 0) {
295
+ html += renderFileSection('Untracked Files', untracked, false, 'untracked');
296
+ }
297
+
298
+ html += '</div>';
299
+ container.innerHTML = html;
300
+
301
+ // Attach click handlers
302
+ attachStatusEventHandlers(container);
303
+ }
304
+
305
+ function renderFileSection(title, files, isStaged, type) {
306
+ return `
307
+ <div class="git-section git-section-${type}">
308
+ <div class="git-section-header">
309
+ <span>${title}</span>
310
+ <span class="git-section-count">${files.length}</span>
311
+ </div>
312
+ <div class="git-file-list">
313
+ ${files.map(f => renderFileItem(f, isStaged, type)).join('')}
314
+ </div>
315
+ </div>
316
+ `;
317
+ }
318
+
319
+ function renderFileItem(file, isStaged, type) {
320
+ const state = getState();
321
+ const status = file.status || (type === 'untracked' ? 'untracked' : 'modified');
322
+ const statusInfo = STATUS_ICONS[status] || STATUS_ICONS.modified;
323
+ const isClickable = type !== 'untracked';
324
+
325
+ // Show diff stats if available (not for untracked files)
326
+ const hasStats = type !== 'untracked' && (file.additions > 0 || file.deletions > 0);
327
+ const statsHtml = hasStats ? `
328
+ <span class="git-file-stats">
329
+ ${file.additions > 0 ? `<span class="added">+${file.additions}</span>` : ''}
330
+ ${file.deletions > 0 ? `<span class="removed">-${file.deletions}</span>` : ''}
331
+ </span>
332
+ ` : '';
333
+
334
+ // data-cwd is read by the document-level context-menu handler in app.js
335
+ // (showGitFileMenu) so it can resolve relative paths to absolute ones.
336
+ return `
337
+ <div class="git-file-item ${isClickable ? 'clickable' : ''}"
338
+ data-path="${escapeHtml(file.path)}"
339
+ data-cwd="${escapeHtml(state.cwd || '')}"
340
+ data-staged="${isStaged}"
341
+ ${!isClickable ? 'data-no-diff="true"' : ''}>
342
+ <span class="git-file-status" style="color: ${statusInfo.color}" data-tooltip="${statusInfo.label}">
343
+ ${statusInfo.icon}
344
+ </span>
345
+ <span class="git-file-path">${escapeHtml(file.path)}</span>
346
+ ${statsHtml}
347
+ ${isClickable ? `<span class="git-file-chevron">${ICONS.chevronRight}</span>` : ''}
348
+ </div>
349
+ `;
350
+ }
351
+
352
+ function attachStatusEventHandlers(container) {
353
+ container.querySelectorAll('.git-file-item.clickable').forEach(item => {
354
+ item.addEventListener('click', () => {
355
+ const filePath = item.dataset.path;
356
+ const staged = item.dataset.staged === 'true';
357
+ showFileDiff(filePath, staged);
358
+ });
359
+ });
360
+ }
361
+
362
+ // ─────────────────────────────────────────────────────────────────────
363
+ // Diff View
364
+ // ─────────────────────────────────────────────────────────────────────
365
+
366
+ async function showFileDiff(filePath, staged = false) {
367
+ const state = getState();
368
+ state.selectedFile = filePath;
369
+ state.selectedFileStaged = staged;
370
+ state.view = 'diff';
371
+
372
+ const widget = WidgetManager.get('git');
373
+ const container = (widget?.isVisible ? widget.contentContainer : null) || state.currentContainer;
374
+ if (!container) return;
375
+
376
+ container.innerHTML = '<div class="git-loading">Loading diff...</div>';
377
+
378
+ const diff = await loadDiff(filePath, staged);
379
+ const file = diff?.files?.[0] || null;
380
+
381
+ renderDiffView(container, filePath, file, staged);
382
+ }
383
+
384
+ function renderDiffView(container, filePath, file, staged) {
385
+ const state = getState();
386
+ const filename = basename(filePath);
387
+ const statsHtml = file ? `
388
+ <span class="git-diff-stats">
389
+ <span class="added">+${file.additions}</span>
390
+ <span class="removed">-${file.deletions}</span>
391
+ </span>
392
+ ` : '';
393
+
394
+ let diffHtml;
395
+ if (!file || file.hunks.length === 0) {
396
+ diffHtml = '<div class="git-no-diff">No diff available (file may be binary or new)</div>';
397
+ } else {
398
+ diffHtml = renderDiffHunks(file.hunks);
399
+ }
400
+
401
+ container.innerHTML = `
402
+ <div class="git-diff-view">
403
+ <div class="git-diff-header">
404
+ <button class="git-back-btn" data-tooltip="Back">${ICONS.back}</button>
405
+ <span class="git-diff-title">${escapeHtml(filename)}</span>
406
+ ${staged ? '<span class="git-diff-staged">Staged</span>' : ''}
407
+ ${statsHtml}
408
+ <button class="git-sbs-btn" data-tooltip="${S.widgets.diff_viewer.side_by_side}">${ICONS.columns}</button>
409
+ </div>
410
+ <div class="git-diff-container">
411
+ ${diffHtml}
412
+ </div>
413
+ </div>
414
+ `;
415
+
416
+ // Back button handler
417
+ container.querySelector('.git-back-btn')?.addEventListener('click', () => {
418
+ state.view = 'status';
419
+ state.selectedFile = null;
420
+ renderContent();
421
+ });
422
+
423
+ // Side-by-side button handler
424
+ container.querySelector('.git-sbs-btn')?.addEventListener('click', async () => {
425
+ try {
426
+ const cwd = state.cwd || WidgetManager.currentCwd;
427
+ const resp = await fetch(`${CONFIG.API_BASE}/api/git/file-content?file=${encodeURIComponent(filePath)}&cwd=${encodeURIComponent(cwd)}&staged=${staged || false}`);
428
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
429
+ const data = await resp.json();
430
+ window.DiffViewerWidget?.openWithContent(filePath, data.old, data.new, {
431
+ source: 'git',
432
+ oldLabel: data.oldLabel || 'HEAD',
433
+ newLabel: data.newLabel || (staged ? 'Staged' : 'Working Tree')
434
+ });
435
+ } catch (err) {
436
+ console.error('[GitWidget] Failed to load file content for side-by-side:', err);
437
+ }
438
+ });
439
+ }
440
+
441
+ function renderDiffHunks(hunks) {
442
+ return hunks.map(hunk => `
443
+ <div class="git-hunk">
444
+ <div class="git-hunk-header">${escapeHtml(hunk.header)}</div>
445
+ <div class="git-hunk-lines">
446
+ ${hunk.lines.map(line => renderDiffLine(line)).join('')}
447
+ </div>
448
+ </div>
449
+ `).join('');
450
+ }
451
+
452
+ function renderDiffLine(line) {
453
+ let type = 'context';
454
+ if (line.startsWith('+')) type = 'added';
455
+ else if (line.startsWith('-')) type = 'removed';
456
+
457
+ return `<div class="git-diff-line ${type}">${escapeHtml(line) || ' '}</div>`;
458
+ }
459
+
460
+ // ─────────────────────────────────────────────────────────────────────
461
+ // History View
462
+ // ─────────────────────────────────────────────────────────────────────
463
+
464
+ function showHistory() {
465
+ const state = getState();
466
+ state.view = 'history';
467
+ state.selectedCommit = null;
468
+ loadHistory(0);
469
+ }
470
+
471
+ function renderHistoryView(container) {
472
+ const state = getState();
473
+ if (state.commits.length === 0) {
474
+ container.innerHTML = '<div class="git-empty">No commits yet</div>';
475
+ return;
476
+ }
477
+
478
+ container.innerHTML = `
479
+ <div class="git-history-view">
480
+ <div class="git-history-header">
481
+ <button class="git-back-btn" data-tooltip="Back to Status">${ICONS.back}</button>
482
+ <span class="git-history-title">Commit History</span>
483
+ </div>
484
+ <div class="git-commit-list">
485
+ ${state.commits.map(c => renderCommitItem(c)).join('')}
486
+ </div>
487
+ ${state.hasMoreCommits ? '<button class="git-load-more">Load More</button>' : ''}
488
+ </div>
489
+ `;
490
+
491
+ // Event handlers
492
+ container.querySelector('.git-back-btn')?.addEventListener('click', () => {
493
+ state.view = 'status';
494
+ renderContent();
495
+ });
496
+
497
+ container.querySelectorAll('.git-commit-item').forEach(item => {
498
+ item.addEventListener('click', () => {
499
+ const hash = item.dataset.hash;
500
+ showCommitDetail(hash);
501
+ });
502
+ });
503
+
504
+ container.querySelector('.git-load-more')?.addEventListener('click', () => {
505
+ loadHistory(state.commits.length);
506
+ });
507
+ }
508
+
509
+ function renderCommitItem(commit) {
510
+ const relTime = formatRelativeTime(commit.date);
511
+ const subjectShort = commit.subject.length > 60
512
+ ? commit.subject.slice(0, 60) + '...'
513
+ : commit.subject;
514
+
515
+ return `
516
+ <div class="git-commit-item" data-hash="${commit.hash}">
517
+ <div class="git-commit-hash">${escapeHtml(commit.hashShort)}</div>
518
+ <div class="git-commit-info">
519
+ <div class="git-commit-subject">${escapeHtml(subjectShort)}</div>
520
+ <div class="git-commit-meta">
521
+ <span class="git-commit-author">${escapeHtml(commit.author)}</span>
522
+ <span class="git-commit-time">${relTime}</span>
523
+ </div>
524
+ </div>
525
+ <span class="git-commit-chevron">${ICONS.chevronRight}</span>
526
+ </div>
527
+ `;
528
+ }
529
+
530
+ // ─────────────────────────────────────────────────────────────────────
531
+ // Commit Detail View
532
+ // ─────────────────────────────────────────────────────────────────────
533
+
534
+ async function showCommitDetail(hash) {
535
+ const state = getState();
536
+ state.selectedCommit = hash;
537
+ state.view = 'commit';
538
+
539
+ const widget = WidgetManager.get('git');
540
+ const container = (widget?.isVisible ? widget.contentContainer : null) || state.currentContainer;
541
+ if (!container) return;
542
+
543
+ container.innerHTML = '<div class="git-loading">Loading commit...</div>';
544
+
545
+ const data = await loadCommit(hash);
546
+ if (!data) {
547
+ container.innerHTML = '<div class="git-error">Failed to load commit</div>';
548
+ return;
549
+ }
550
+
551
+ renderCommitDetailView(container, data);
552
+ }
553
+
554
+ function renderCommitDetailView(container, data) {
555
+ const state = getState();
556
+ const { commit, files } = data;
557
+ const relTime = formatRelativeTime(commit.timestamp ? new Date(commit.timestamp * 1000).toISOString() : null);
558
+
559
+ // Calculate totals
560
+ const totalAdditions = files.reduce((sum, f) => sum + f.additions, 0);
561
+ const totalDeletions = files.reduce((sum, f) => sum + f.deletions, 0);
562
+
563
+ // Check if body is long enough to need truncation
564
+ const bodyLines = commit.body ? commit.body.split('\n') : [];
565
+ const needsTruncation = bodyLines.length > 4;
566
+
567
+ container.innerHTML = `
568
+ <div class="git-commit-detail">
569
+ <div class="git-commit-detail-header">
570
+ <button class="git-back-btn" data-tooltip="Back to History">${ICONS.back}</button>
571
+ <span class="git-commit-detail-hash">${escapeHtml(commit.hashShort)}</span>
572
+ </div>
573
+
574
+ <div class="git-commit-detail-info">
575
+ <div class="git-commit-detail-subject">${escapeHtml(commit.subject)}</div>
576
+ ${commit.body ? `<div class="git-commit-detail-body${needsTruncation ? ' is-truncated' : ''}">${escapeHtml(commit.body)}</div>` : ''}
577
+ ${needsTruncation ? '<button class="git-body-expand-btn">Show more</button>' : ''}
578
+ <div class="git-commit-detail-meta">
579
+ <span>${escapeHtml(commit.author)}</span>
580
+ <span>${relTime}</span>
581
+ </div>
582
+ </div>
583
+
584
+ <div class="git-commit-files-header">
585
+ <span>${files.length} file${files.length !== 1 ? 's' : ''} changed</span>
586
+ <span class="git-commit-stats">
587
+ <span class="added">+${totalAdditions}</span>
588
+ <span class="removed">-${totalDeletions}</span>
589
+ </span>
590
+ </div>
591
+
592
+ <div class="git-commit-files">
593
+ ${files.map(f => `
594
+ <div class="git-commit-file" data-path="${escapeHtml(f.path)}" data-cwd="${escapeHtml(state.cwd || '')}">
595
+ <span class="git-commit-file-path">${escapeHtml(f.path)}</span>
596
+ <span class="git-commit-file-right">
597
+ <span class="git-commit-file-stats">
598
+ <span class="added">+${f.additions}</span>
599
+ <span class="removed">-${f.deletions}</span>
600
+ </span>
601
+ <button class="git-commit-file-diff-btn" data-path="${escapeHtml(f.path)}" data-tooltip="View diff">${ICONS.code}</button>
602
+ </span>
603
+ </div>
604
+ `).join('')}
605
+ </div>
606
+ </div>
607
+ `;
608
+
609
+ // Event handlers
610
+ container.querySelector('.git-back-btn')?.addEventListener('click', () => {
611
+ showHistory();
612
+ });
613
+
614
+ // Expand/collapse commit body
615
+ container.querySelector('.git-body-expand-btn')?.addEventListener('click', (e) => {
616
+ const body = container.querySelector('.git-commit-detail-body');
617
+ if (body) {
618
+ body.classList.toggle('is-truncated');
619
+ e.target.textContent = body.classList.contains('is-truncated') ? 'Show more' : 'Show less';
620
+ }
621
+ });
622
+
623
+ // Per-file diff buttons → open in DiffViewerWidget
624
+ container.querySelectorAll('.git-commit-file-diff-btn').forEach(btn => {
625
+ btn.addEventListener('click', (e) => {
626
+ e.stopPropagation();
627
+ openCommitFileDiff(commit.hash, btn.dataset.path);
628
+ });
629
+ });
630
+
631
+ // Click file row also opens diff
632
+ container.querySelectorAll('.git-commit-file').forEach(item => {
633
+ item.addEventListener('click', () => {
634
+ openCommitFileDiff(commit.hash, item.dataset.path);
635
+ });
636
+ });
637
+ }
638
+
639
+ async function openCommitFileDiff(commitHash, filePath) {
640
+ const state = getState();
641
+ const cwd = state.cwd || WidgetManager.currentCwd;
642
+ try {
643
+ const resp = await fetch(`${CONFIG.API_BASE}/api/git/file-content?file=${encodeURIComponent(filePath)}&cwd=${encodeURIComponent(cwd)}&commit=${encodeURIComponent(commitHash)}`);
644
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
645
+ const data = await resp.json();
646
+ window.DiffViewerWidget?.openWithContent(filePath, data.old, data.new, {
647
+ source: 'git',
648
+ oldLabel: data.oldLabel,
649
+ newLabel: data.newLabel
650
+ });
651
+ } catch (err) {
652
+ console.error('[GitWidget] Failed to open commit file diff:', err);
653
+ }
654
+ }
655
+
656
+
657
+ // ─────────────────────────────────────────────────────────────────────
658
+ // Helpers
659
+ // ─────────────────────────────────────────────────────────────────────
660
+
661
+ // ─────────────────────────────────────────────────────────────────────
662
+ // Widget Registration
663
+ // ─────────────────────────────────────────────────────────────────────
664
+
665
+ /**
666
+ * Register the Git Widget
667
+ */
668
+ export function registerGitWidget() {
669
+ WidgetManager.register('git', {
670
+ type: 'bottom-sheet',
671
+ title: S.widgets.titles.git,
672
+ icon: 'git',
673
+ shortcut: 'Alt+G',
674
+
675
+ // Device-specific types
676
+ deviceTypes: {
677
+ default: 'bottom-sheet',
678
+ phone: 'bottom-sheet',
679
+ tablet: 'bottom-sheet',
680
+ desktop: 'sidebar-right' // Sidebar on desktop
681
+ },
682
+
683
+ // Heights for bottom-sheet
684
+ heights: {
685
+ half: '45vh',
686
+ full: '85vh'
687
+ },
688
+
689
+ // Sidebar width (wider than default for better readability)
690
+ width: '380px',
691
+ minWidth: '280px',
692
+ maxWidth: '600px',
693
+
694
+ // Allow transform to these types
695
+ allowedTypes: ['bottom-sheet', 'sidebar-right', 'floating', 'tab'],
696
+
697
+ // Custom header actions
698
+ headerActions: [
699
+ {
700
+ icon: 'history',
701
+ title: S.widgets.header_actions.commit_history,
702
+ onClick: () => showHistory()
703
+ },
704
+ {
705
+ icon: 'refresh',
706
+ title: S.widgets.header_actions.refresh,
707
+ onClick: () => refresh()
708
+ }
709
+ ],
710
+
711
+ // Render function
712
+ render: (container, ctx) => {
713
+ const state = getState(ctx.sessionId);
714
+ // Store container for tab mode
715
+ state.currentContainer = container;
716
+
717
+ // Update state from context, with fallback to active session
718
+ const newCwd = ctx.cwd || window.app?.activeSession?.cwd;
719
+ if (newCwd !== state.cwd) {
720
+ state.cwd = newCwd;
721
+ state.reset();
722
+ }
723
+
724
+ // Load status if we have cwd and this is tab mode
725
+ if (ctx.isTab && state.cwd && !state.gitStatus) {
726
+ loadStatus();
727
+ }
728
+
729
+ renderContent();
730
+ },
731
+
732
+ // Open handler - always refresh on open
733
+ onOpen: () => {
734
+ const state = getState();
735
+ // Fallback to get cwd from active session if not set
736
+ if (!state.cwd && window.app?.activeSession?.cwd) {
737
+ state.cwd = window.app.activeSession.cwd;
738
+ }
739
+
740
+ if (state.cwd && !state.loading) {
741
+ refresh();
742
+ }
743
+ },
744
+
745
+ // Transform handler
746
+ onTransform: (fromType, toType) => {
747
+ renderContent();
748
+ },
749
+
750
+ // Cleanup when session is closed
751
+ onDestroy: (sessionId) => {
752
+ destroyState(sessionId);
753
+ }
754
+ });
755
+ }
756
+
757
+ // ─────────────────────────────────────────────────────────────────────
758
+ // Public API
759
+ // ─────────────────────────────────────────────────────────────────────
760
+
761
+ /**
762
+ * Public API for external access
763
+ */
764
+ export const GitWidget = {
765
+ /**
766
+ * Refresh the current view
767
+ */
768
+ refresh,
769
+
770
+ /**
771
+ * Show commit history
772
+ */
773
+ showHistory,
774
+
775
+ /**
776
+ * Show diff for a specific file
777
+ */
778
+ showFileDiff,
779
+
780
+ /**
781
+ * Show details for a specific commit
782
+ */
783
+ showCommitDetail,
784
+
785
+ /**
786
+ * Get current state (for debugging)
787
+ */
788
+ getState: () => ({ ...getState() }),
789
+
790
+ /**
791
+ * Destroy state for a session
792
+ */
793
+ destroyState
794
+ };