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,532 @@
1
+ /**
2
+ * xterm.js link providers — URL detection (handling wrapped lines) and
3
+ * file-path detection (with on-click resolution via /api/find-file).
4
+ * Plus the lazy `loadXterm()` loader for the vendored xterm bundle.
5
+ *
6
+ * Both providers are pure xterm-addon classes — they don't reach back
7
+ * into TerminalState. init.js wires them onto the terminal instance and
8
+ * hands the file provider a `getCwd` closure (plus an optional
9
+ * `getLiveCwd` refresher — see makeLiveCwdRefresher) so it stays in
10
+ * sync when the session's CWD changes.
11
+ *
12
+ * Resolution is context-aware: bare filenames are resolved with the
13
+ * shell's live cwd and with directory hints scraped from the terminal
14
+ * lines above the click (e.g. the `ll docs-ai/readme/` command that
15
+ * produced a bare-filename listing) — see collectDirHints().
16
+ */
17
+
18
+ import { CONFIG } from '../../config.js';
19
+ import { showToast } from '../../context-menu.js';
20
+ import { openExternal } from '../../utils.js';
21
+ import {
22
+ buildPathPattern,
23
+ buildStandalonePattern,
24
+ buildUrlPattern,
25
+ parseLineInfo,
26
+ isValidStandaloneFile,
27
+ cleanUrlTrailingPunct
28
+ } from '../../linkify-utils.js';
29
+ import { isContinuationRow } from './wrap-utils.js';
30
+ import { isAbsolutePath } from '../../path-utils.js';
31
+
32
+ // ─────────────────────────────────────────────────────────────────────
33
+ // xterm.js Loading
34
+ // ─────────────────────────────────────────────────────────────────────
35
+
36
+ export async function loadXterm() {
37
+ // Check if already loaded
38
+ if (window.Terminal && window.FitAddon) {
39
+ return;
40
+ }
41
+
42
+ // Load CSS
43
+ if (!document.querySelector('link[href*="xterm.css"]')) {
44
+ const link = document.createElement('link');
45
+ link.rel = 'stylesheet';
46
+ link.href = '/static/vendor/xterm.css';
47
+ document.head.appendChild(link);
48
+ }
49
+
50
+ // Load xterm.js
51
+ if (!window.Terminal) {
52
+ await loadScript('/static/vendor/xterm.js');
53
+ }
54
+
55
+ // Load fit addon
56
+ if (!window.FitAddon) {
57
+ await loadScript('/static/vendor/xterm-addon-fit.js');
58
+ }
59
+ }
60
+
61
+ function loadScript(src) {
62
+ return new Promise((resolve, reject) => {
63
+ const script = document.createElement('script');
64
+ script.src = src;
65
+ script.onload = resolve;
66
+ script.onerror = reject;
67
+ document.head.appendChild(script);
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Build a refresher that fetches the shell's live cwd for a terminal
73
+ * state (/api/terminal-cwd reads /proc/<pid>/cwd, so it tracks the
74
+ * user's `cd`s). The result is cached on `state.liveCwd` — deliberately
75
+ * NOT written to `state.cwd`: the WS session key is derived from cwd,
76
+ * so mutating it would re-key the PTY on reconnect instead of
77
+ * reattaching to the running shell.
78
+ */
79
+ export function makeLiveCwdRefresher(state) {
80
+ return async () => {
81
+ if (!state.sessionId) return null;
82
+ try {
83
+ const url = `${CONFIG.API_BASE}/api/terminal-cwd?session=${encodeURIComponent(state.sessionId)}`;
84
+ const response = await fetch(url);
85
+ if (!response.ok) return null;
86
+ const data = await response.json();
87
+ if (data.cwd) state.liveCwd = data.cwd;
88
+ return data.cwd || null;
89
+ } catch (_) {
90
+ return null;
91
+ }
92
+ };
93
+ }
94
+
95
+ // ─────────────────────────────────────────────────────────────────────
96
+ // URL Link Provider — handles URLs that span wrapped terminal lines
97
+ // ─────────────────────────────────────────────────────────────────────
98
+
99
+ /**
100
+ * Custom link provider for URLs that span wrapped terminal lines.
101
+ * Replaces WebLinksAddon which only detects URLs within a single line.
102
+ *
103
+ * Row-continuation detection is shared with the copy paths — see
104
+ * wrap-utils.js `isContinuationRow()` for the three wrapping scenarios
105
+ * (terminal isWrapped, OSC 8 id spanning the boundary, shell-input
106
+ * full-width heuristic).
107
+ */
108
+ export class UrlLinkProvider {
109
+ constructor(terminal) {
110
+ this.terminal = terminal;
111
+ }
112
+
113
+ provideLinks(lineNumber, callback) {
114
+ const buffer = this.terminal.buffer.active;
115
+ const line = buffer.getLine(lineNumber - 1);
116
+ if (!line) {
117
+ callback(undefined);
118
+ return;
119
+ }
120
+
121
+ const lineTexts = [];
122
+ const lineNumbers = [];
123
+ let currentLineNum = lineNumber;
124
+
125
+ // Walk back to find the start of the logical line sequence.
126
+ // (isContinuationRow takes 0-based row indices; lineNumber is 1-based.)
127
+ while (currentLineNum > 1 && isContinuationRow(this.terminal, currentLineNum - 1)) {
128
+ currentLineNum--;
129
+ }
130
+
131
+ // Collect all lines in this logical line sequence (always from start)
132
+ let currentLine = buffer.getLine(currentLineNum - 1);
133
+ while (currentLine) {
134
+ let text = currentLine.translateToString(false);
135
+ const joinsNext = isContinuationRow(this.terminal, currentLineNum);
136
+ const nextLine = joinsNext ? buffer.getLine(currentLineNum) : null;
137
+ if (nextLine && !nextLine.isWrapped) {
138
+ // TUI-emitted continuation (OSC 8 / heuristic join): the row
139
+ // may be shorter than the terminal width, and untrimmed text
140
+ // pads it with blank cells — which would inject spaces into
141
+ // the joined URL. Strip the padding; a URL never ends in
142
+ // whitespace, so nothing legitimate is lost.
143
+ text = text.replace(/\s+$/, '');
144
+ }
145
+ lineTexts.push(text);
146
+ lineNumbers.push(currentLineNum);
147
+
148
+ if (nextLine) {
149
+ currentLineNum++;
150
+ currentLine = nextLine;
151
+ } else {
152
+ break;
153
+ }
154
+ }
155
+
156
+ const fullText = lineTexts.join('');
157
+ if (!fullText.trim()) {
158
+ callback(undefined);
159
+ return;
160
+ }
161
+
162
+ // Convert character offset in combined text to multi-line range
163
+ const offsetToRange = (startOffset, length) => {
164
+ const endOffset = startOffset + length;
165
+ let charCount = 0;
166
+ let startLine = lineNumbers[0], startCol = startOffset + 1;
167
+ for (let i = 0; i < lineTexts.length; i++) {
168
+ const lineLen = lineTexts[i].length;
169
+ if (charCount + lineLen > startOffset) {
170
+ startLine = lineNumbers[i];
171
+ startCol = (startOffset - charCount) + 1;
172
+ break;
173
+ }
174
+ charCount += lineLen;
175
+ }
176
+ charCount = 0;
177
+ let endLine = lineNumbers[lineNumbers.length - 1], endCol = endOffset;
178
+ for (let i = 0; i < lineTexts.length; i++) {
179
+ const lineLen = lineTexts[i].length;
180
+ if (charCount + lineLen >= endOffset) {
181
+ endLine = lineNumbers[i];
182
+ endCol = (endOffset - charCount);
183
+ break;
184
+ }
185
+ charCount += lineLen;
186
+ }
187
+ return { start: { x: startCol, y: startLine }, end: { x: endCol, y: endLine } };
188
+ };
189
+
190
+ const links = [];
191
+ const urlPattern = buildUrlPattern();
192
+ let match;
193
+ while ((match = urlPattern.exec(fullText)) !== null) {
194
+ const { url } = cleanUrlTrailingPunct(match[0]);
195
+ links.push({
196
+ range: offsetToRange(match.index, url.length),
197
+ text: url,
198
+ activate: (event, linkText) => {
199
+ if (event.button === 2) return;
200
+ // Anchor-click, not window.open — the latter silently
201
+ // no-ops in the iPad standalone PWA (see openExternal)
202
+ openExternal(url);
203
+ }
204
+ });
205
+ }
206
+
207
+ callback(links.length > 0 ? links : undefined);
208
+ }
209
+ }
210
+
211
+ // ─────────────────────────────────────────────────────────────────────
212
+ // File Path Link Provider
213
+ // ─────────────────────────────────────────────────────────────────────
214
+
215
+ /**
216
+ * Custom link provider for file paths in terminal output.
217
+ * Uses xterm.js's ILinkProvider interface to detect and handle file path clicks.
218
+ *
219
+ * Detection happens on-demand (when xterm needs links for a line).
220
+ * Verification happens on-click (to avoid performance overhead).
221
+ */
222
+ export class FilePathLinkProvider {
223
+ constructor(terminal, getCwd, getLiveCwd = null) {
224
+ this.terminal = terminal;
225
+ this.getCwd = getCwd; // Function to get current CWD (may change)
226
+ this.getLiveCwd = getLiveCwd; // Optional async refresher for the shell's live cwd
227
+ this.pathCache = new Map(); // Cache verified paths: path -> resolved
228
+ }
229
+
230
+ /**
231
+ * Called by xterm.js when it needs links for a buffer line.
232
+ * @param {number} lineNumber - 1-based line number in buffer
233
+ * @param {function} callback - Callback with array of links or undefined
234
+ */
235
+ provideLinks(lineNumber, callback) {
236
+ const buffer = this.terminal.buffer.active;
237
+ // xterm passes 1-based line numbers; buffer.getLine() uses 0-based
238
+ const line = buffer.getLine(lineNumber - 1);
239
+ if (!line) {
240
+ callback(undefined);
241
+ return;
242
+ }
243
+
244
+ // Get terminal width for wrapped line handling
245
+
246
+ // Collect the full sequence of wrapped lines starting from this line
247
+ // A line is "wrapped" if the next line has isWrapped=true
248
+ // NOTE: lineNumber/currentLineNum stay 1-based (for xterm range y-coordinates)
249
+ const lineTexts = [];
250
+ const lineNumbers = [];
251
+ let currentLineNum = lineNumber;
252
+ let currentLine = line;
253
+
254
+ // First, check if this line is a continuation (isWrapped) - if so, find the start
255
+ while (currentLineNum > 1) {
256
+ const checkLine = buffer.getLine(currentLineNum - 1);
257
+ if (checkLine && checkLine.isWrapped) {
258
+ currentLineNum--;
259
+ } else {
260
+ break;
261
+ }
262
+ }
263
+
264
+ // Now collect all lines in this wrapped sequence
265
+ const startLineNum = currentLineNum;
266
+ currentLine = buffer.getLine(currentLineNum - 1);
267
+
268
+ while (currentLine) {
269
+ lineTexts.push(currentLine.translateToString(false)); // false = don't trim right
270
+ lineNumbers.push(currentLineNum);
271
+
272
+ // Check if next line continues the wrap (0-based: currentLineNum+1-1 = currentLineNum)
273
+ const nextLine = buffer.getLine(currentLineNum);
274
+ if (nextLine && nextLine.isWrapped) {
275
+ currentLineNum++;
276
+ currentLine = nextLine;
277
+ } else {
278
+ break;
279
+ }
280
+ }
281
+
282
+ // Only return links when called for the START of a wrapped sequence
283
+ // This prevents duplicate link detection for continuation lines
284
+ if (lineNumber !== startLineNum) {
285
+ callback(undefined);
286
+ return;
287
+ }
288
+
289
+ // Combine all wrapped lines into single text
290
+ const fullText = lineTexts.join('');
291
+ if (!fullText.trim()) {
292
+ callback(undefined);
293
+ return;
294
+ }
295
+
296
+ const links = [];
297
+ const seenRanges = new Set(); // Avoid duplicate links at same position
298
+
299
+ /**
300
+ * Convert a character offset in the combined text to multi-line range
301
+ */
302
+ const offsetToRange = (startOffset, length) => {
303
+ const endOffset = startOffset + length;
304
+
305
+ // Find start line and column
306
+ let charCount = 0;
307
+ let startLine = lineNumbers[0];
308
+ let startCol = startOffset + 1; // 1-based
309
+
310
+ for (let i = 0; i < lineTexts.length; i++) {
311
+ const lineLen = lineTexts[i].length;
312
+ if (charCount + lineLen > startOffset) {
313
+ startLine = lineNumbers[i];
314
+ startCol = (startOffset - charCount) + 1;
315
+ break;
316
+ }
317
+ charCount += lineLen;
318
+ }
319
+
320
+ // Find end line and column (endOffset is exclusive, xterm end.x is inclusive)
321
+ charCount = 0;
322
+ let endLine = lineNumbers[lineNumbers.length - 1];
323
+ let endCol = endOffset;
324
+
325
+ for (let i = 0; i < lineTexts.length; i++) {
326
+ const lineLen = lineTexts[i].length;
327
+ if (charCount + lineLen >= endOffset) {
328
+ endLine = lineNumbers[i];
329
+ endCol = (endOffset - charCount);
330
+ break;
331
+ }
332
+ charCount += lineLen;
333
+ }
334
+
335
+ return {
336
+ start: { x: startCol, y: startLine },
337
+ end: { x: endCol, y: endLine }
338
+ };
339
+ };
340
+
341
+ // Match paths with directories (e.g., src/app.js, ./config.yaml)
342
+ const pathPattern = buildPathPattern();
343
+ let match;
344
+ while ((match = pathPattern.exec(fullText)) !== null) {
345
+ const path = match[1];
346
+ const lineInfo = match[2] || '';
347
+ const fullMatch = match[0];
348
+
349
+ // Skip invalid patterns
350
+ if (!path.includes('/')) continue;
351
+ if (/^\d+\.\d+/.test(path)) continue; // Version numbers
352
+ if (/^\d+\/\d+/.test(path)) continue; // Fractions like 1/2
353
+
354
+ const rangeKey = `${match.index}-${match.index + fullMatch.length}`;
355
+ if (seenRanges.has(rangeKey)) continue;
356
+ seenRanges.add(rangeKey);
357
+
358
+ const range = offsetToRange(match.index, fullMatch.length);
359
+ links.push({
360
+ range,
361
+ text: fullMatch,
362
+ activate: (event, linkText) => this.handleClick(path, lineInfo, event, range.start.y)
363
+ });
364
+ }
365
+
366
+ // Match standalone filenames (e.g., server.py, CLAUDE.md)
367
+ const standalonePattern = buildStandalonePattern();
368
+ while ((match = standalonePattern.exec(fullText)) !== null) {
369
+ const filename = match[1];
370
+ const lineInfo = match[2] || '';
371
+ const fullMatch = match[0];
372
+
373
+ // Get preceding text for context validation
374
+ const precedingText = fullText.slice(Math.max(0, match.index - 20), match.index);
375
+
376
+ // Validate the match
377
+ if (!isValidStandaloneFile(filename, precedingText)) {
378
+ continue;
379
+ }
380
+
381
+ const rangeKey = `${match.index}-${match.index + fullMatch.length}`;
382
+ if (seenRanges.has(rangeKey)) continue;
383
+ seenRanges.add(rangeKey);
384
+
385
+ const range = offsetToRange(match.index, fullMatch.length);
386
+ links.push({
387
+ range,
388
+ text: fullMatch,
389
+ activate: (event, linkText) => this.handleClick(filename, lineInfo, event, range.start.y)
390
+ });
391
+ }
392
+
393
+ callback(links.length > 0 ? links : undefined);
394
+ }
395
+
396
+ /**
397
+ * Scan the terminal lines above a link for directory context — e.g.
398
+ * the `ll docs-ai/readme/` command whose output is a bare-filename
399
+ * listing, or an earlier `cd some/dir`. Returns candidate directories
400
+ * (nearest line first, deduped, capped) that the server tries before
401
+ * falling back to a project-wide search.
402
+ *
403
+ * @param {number} startLine - 1-based buffer line the link starts on
404
+ * @returns {string[]} directory hints, most relevant first
405
+ */
406
+ collectDirHints(startLine, maxLines = 200, maxHints = 8) {
407
+ const buffer = this.terminal?.buffer?.active;
408
+ if (!buffer || !startLine) return [];
409
+
410
+ const hints = [];
411
+ const seen = new Set();
412
+ const add = (dir) => {
413
+ dir = dir.replace(/^\.\//, '').replace(/\/+$/, '');
414
+ if (!dir || dir === '.' || dir === '..' || dir === '~') return;
415
+ if (seen.has(dir)) return;
416
+ seen.add(dir);
417
+ hints.push(dir);
418
+ };
419
+
420
+ const pattern = buildPathPattern();
421
+ const lowest = Math.max(1, startLine - maxLines);
422
+ for (let y = startLine; y >= lowest && hints.length < maxHints; y--) {
423
+ const text = buffer.getLine(y - 1)?.translateToString(true);
424
+ if (!text || !text.includes('/')) continue;
425
+
426
+ pattern.lastIndex = 0;
427
+ let match;
428
+ while ((match = pattern.exec(text)) !== null && hints.length < maxHints) {
429
+ const token = match[1];
430
+ if (!token.includes('/')) continue;
431
+ if (token.endsWith('/')) {
432
+ add(token); // explicit directory (trailing slash)
433
+ } else {
434
+ const parent = token.slice(0, token.lastIndexOf('/'));
435
+ if (parent) add(parent); // parent dir of a path-like token
436
+ add(token); // token itself may be a dir (`ls docs-ai/readme`)
437
+ }
438
+ }
439
+ }
440
+ return hints;
441
+ }
442
+
443
+ /**
444
+ * Resolve a path string via /api/find-file, passing any context
445
+ * hints along. Returns { path, isDir } with the resolved absolute
446
+ * path, or null if not found / on error. Absolute and ~ paths go
447
+ * through the server too so they get verified and classified as
448
+ * file vs directory. Result is cached per (cwd, path, hints).
449
+ */
450
+ async resolveFullPath(path, cwd, hints = []) {
451
+ if (!path) return null;
452
+ const isAbsolute = isAbsolutePath(path) || path.startsWith('~');
453
+ if (!cwd && !isAbsolute) return null;
454
+
455
+ const cacheKey = `${cwd}:${path}:${hints.join('|')}`;
456
+ let resolved = this.pathCache.get(cacheKey);
457
+ if (resolved !== undefined) return resolved;
458
+
459
+ try {
460
+ const params = new URLSearchParams({ name: path });
461
+ if (cwd) params.set('cwd', cwd);
462
+ for (const h of hints) params.append('hint', h);
463
+ const response = await fetch(`${CONFIG.API_BASE}/api/find-file?${params}`);
464
+ const data = await response.json();
465
+ resolved = (data.found && data.path)
466
+ ? { path: data.path, isDir: !!data.is_dir }
467
+ : null;
468
+ } catch (err) {
469
+ console.error('Failed to resolve file path:', err);
470
+ resolved = null;
471
+ }
472
+ this.pathCache.set(cacheKey, resolved);
473
+ return resolved;
474
+ }
475
+
476
+ /**
477
+ * Handle click on a file path link.
478
+ * Refreshes the shell's live cwd, gathers directory hints from the
479
+ * surrounding terminal lines, verifies the path via the server API,
480
+ * then opens preview — or the file explorer when the path is a
481
+ * directory.
482
+ */
483
+ async handleClick(path, lineInfo, event, lineY = null) {
484
+ // Don't open preview on right-click (context menu handles it)
485
+ if (event && event.button === 2) return;
486
+
487
+ let cwd = this.getCwd();
488
+ if (this.getLiveCwd) {
489
+ const live = await this.getLiveCwd();
490
+ if (live) cwd = live;
491
+ }
492
+ const isAbsolute = isAbsolutePath(path) || path.startsWith('~');
493
+ if (!cwd && !isAbsolute) {
494
+ showToast('No working directory set', 'warning');
495
+ return;
496
+ }
497
+
498
+ try {
499
+ const hints = lineY ? this.collectDirHints(lineY) : [];
500
+ const resolved = await this.resolveFullPath(path, cwd, hints);
501
+ if (resolved) {
502
+ // Directories open in the file explorer, not the preview
503
+ if (resolved.isDir) {
504
+ window.FileExplorerWidget?.navigateTo?.(resolved.path);
505
+ return;
506
+ }
507
+
508
+ // Parse line info for preview options
509
+ const opts = parseLineInfo(lineInfo) || {};
510
+
511
+ // Ctrl/Cmd+click opens in editor, normal click opens preview
512
+ if (event.metaKey || event.ctrlKey) {
513
+ window.app?.openFileInEditor?.(resolved.path);
514
+ } else {
515
+ window.app?.previewFile?.(resolved.path, opts);
516
+ }
517
+ } else {
518
+ showToast(`File not found: ${path}`, 'error');
519
+ }
520
+ } catch (err) {
521
+ console.error('Failed to verify file path:', err);
522
+ showToast(`Error verifying path: ${path}`, 'error');
523
+ }
524
+ }
525
+
526
+ /**
527
+ * Clear the path cache (e.g., when CWD changes)
528
+ */
529
+ clearCache() {
530
+ this.pathCache.clear();
531
+ }
532
+ }