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,879 @@
1
+ /**
2
+ * OpenDialog — fish-style path picker for "Open" flows.
3
+ *
4
+ * The input is a literal path being typed. As the user types, a dim ghost
5
+ * suggestion extends inline showing the best prefix completion of the
6
+ * current segment. Tab or Right-arrow accepts the ghost and continues.
7
+ *
8
+ * Three input shapes, all prefix-based (no fuzzy scoring):
9
+ * - Empty → list cwd's contents
10
+ * - Plain text (no `/`) → prefix-match basenames across the project
11
+ * - Path-ish (has `/`, `~`, `.`) → strict directory listing + prefix filter
12
+ *
13
+ * Enter/click on a folder → drill into it (browse its contents). The
14
+ * auto-selected "Open this folder" row starts a session at the current dir.
15
+ * Enter on a file → preview (or new tab with Cmd/Ctrl+Enter).
16
+ *
17
+ * Works anywhere on the filesystem: `/etc/`, `~/Documents/`, `../`,
18
+ * absolute paths. OS file permissions still apply (the server can only
19
+ * read what its user can read); `/proc`, `/sys`, `/dev` are blocked.
20
+ */
21
+
22
+ import S from './strings.js';
23
+ import { CONFIG } from './config.js';
24
+ import { ICONS } from './widget-system/index.js';
25
+ import { escapeHtml, extractApiError } from './utils.js';
26
+ import { basename, dirname, isAbsolutePath, isUnder, joinPath, parentOf, pathRoot, pathSep, relativeTo, resolvePath, splitPath, stripTrailingSep } from './path-utils.js';
27
+ import { ContextMenu, copyToClipboard, showToast } from './context-menu.js';
28
+
29
+ const DEBOUNCE_MS = 80;
30
+ const PROJECT_CACHE_TTL = 60_000;
31
+ const PATH_CACHE_TTL = 5_000;
32
+ const RECENT_TTL = 30_000;
33
+ const MAX_LIST = 100;
34
+ const MAX_BASENAME_LIST = 50;
35
+
36
+ /**
37
+ * `absPath` under `base`, as text to put back in the input box.
38
+ *
39
+ * relativeTo() normalizes to '/' because its usual consumers are git
40
+ * pathspecs and API keys; here the separator has to match what the user
41
+ * types, so the server's own goes back in. Doing the offset by hand is what
42
+ * broke on a root base ('/', 'C:\'): `base.length + 1` ate the first real
43
+ * character, turning /etc/hosts into tc/hosts.
44
+ */
45
+ function relDisplay(absPath, base) {
46
+ return relativeTo(absPath, base).split('/').join(pathSep(absPath));
47
+ }
48
+
49
+ class OpenDialogClass {
50
+ constructor() {
51
+ this._projectCache = new Map();
52
+ this._pathCache = new Map();
53
+ this._recentCache = new Map();
54
+ this._reqToken = 0;
55
+ this._items = [];
56
+ this._selectedIndex = 0;
57
+ this._ghost = '';
58
+ this._debounceTimer = null;
59
+ this._previousActiveEl = null;
60
+ this._ctxMenu = null;
61
+ this._built = false;
62
+ this._missingDir = null;
63
+ }
64
+
65
+ _build() {
66
+ if (this._built) return;
67
+ this._built = true;
68
+
69
+ const overlay = document.createElement('div');
70
+ overlay.id = 'open-dialog-overlay';
71
+ overlay.className = 'qs-overlay od-overlay';
72
+ overlay.setAttribute('role', 'dialog');
73
+ overlay.setAttribute('aria-modal', 'true');
74
+ overlay.setAttribute('aria-label', S.open_dialog.aria_label);
75
+ overlay.hidden = true;
76
+ overlay.innerHTML = `
77
+ <div class="qs-modal od-modal" role="combobox" aria-expanded="true" aria-haspopup="listbox" aria-owns="od-list">
78
+ <div class="qs-input-wrap od-input-wrap">
79
+ <span class="od-ghost" aria-hidden="true"
80
+ ><span class="od-ghost-typed"></span
81
+ ><span class="od-ghost-rest"></span
82
+ ></span>
83
+ <input type="text" class="qs-input od-input" autocomplete="off" autocapitalize="off"
84
+ autocorrect="off" spellcheck="false"
85
+ data-shortcuts-disabled="true"
86
+ aria-controls="od-list"
87
+ placeholder="${escapeHtml(S.open_dialog.placeholder)}">
88
+ </div>
89
+ <div class="od-path" aria-live="polite"></div>
90
+ <div class="qs-list od-list" id="od-list" role="listbox"></div>
91
+ <div class="qs-footer od-footer">
92
+ <div class="qs-footer-row">
93
+ <span class="qs-hint">${S.open_dialog.hints.nav}</span>
94
+ <span class="qs-hint">${S.open_dialog.hints.parent}</span>
95
+ <span class="qs-hint od-hint-tab">${S.open_dialog.hints.complete}</span>
96
+ <span class="qs-hint od-hint-file">${S.open_dialog.hints.preview}</span>
97
+ <span class="qs-hint od-hint-dir-nav">${S.open_dialog.hints.enter_folder}</span>
98
+ <span class="qs-hint od-hint-dir-create">${S.open_dialog.hints.create_folder}</span>
99
+ <span class="qs-hint od-hint-dir-open">${S.open_dialog.hints.open_session}</span>
100
+ <span class="qs-hint">${S.open_dialog.hints.switcher}</span>
101
+ <span class="qs-hint">${S.open_dialog.hints.close}</span>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ `;
106
+ document.body.appendChild(overlay);
107
+
108
+ this.overlay = overlay;
109
+ this.input = overlay.querySelector('.od-input');
110
+ this.list = overlay.querySelector('.od-list');
111
+ this.footer = overlay.querySelector('.od-footer');
112
+ this.ghostTyped = overlay.querySelector('.od-ghost-typed');
113
+ this.ghostRest = overlay.querySelector('.od-ghost-rest');
114
+ this.pathEl = overlay.querySelector('.od-path');
115
+
116
+ this.input.addEventListener('input', () => this._scheduleChange());
117
+ this.input.addEventListener('keydown', (e) => this._onKey(e));
118
+ overlay.addEventListener('click', (e) => { if (e.target === overlay) this.hide(); });
119
+
120
+ this.list.addEventListener('click', (e) => {
121
+ const it = e.target.closest('.qs-item');
122
+ if (!it) return;
123
+ this._selectedIndex = parseInt(it.dataset.index, 10);
124
+ this._renderList();
125
+ this._submit(this._optsFromEvent(e));
126
+ // If the click drilled into a folder (dialog still open), pull focus
127
+ // back to the input so the user can keep typing / arrowing.
128
+ if (!this.overlay.hidden) this.input.focus();
129
+ });
130
+ this.list.addEventListener('contextmenu', (e) => {
131
+ const it = e.target.closest('.qs-item');
132
+ if (!it) return;
133
+ e.preventDefault();
134
+ this._selectedIndex = parseInt(it.dataset.index, 10);
135
+ this._renderList();
136
+ this._showContextMenu(e.clientX, e.clientY);
137
+ });
138
+ // Swallow contextmenu on the overlay backdrop so the FAB radial doesn't fire.
139
+ overlay.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); });
140
+ }
141
+
142
+ show() {
143
+ this._build();
144
+ if (!this.overlay.hidden) {
145
+ this.input.focus();
146
+ this.input.select();
147
+ return;
148
+ }
149
+ this._previousActiveEl = document.activeElement;
150
+ this.overlay.hidden = false;
151
+ this.input.value = '';
152
+ this._ghost = '';
153
+ this._items = [];
154
+ this._selectedIndex = 0;
155
+ const { mode, dir } = this._parse('');
156
+ this._renderCurrentPath(mode, dir);
157
+ this._renderGhost();
158
+ this._renderList();
159
+ requestAnimationFrame(() => {
160
+ this.input.focus();
161
+ this._refresh();
162
+ });
163
+ }
164
+
165
+ hide() {
166
+ if (!this._built || this.overlay.hidden) return;
167
+ this.overlay.hidden = true;
168
+ clearTimeout(this._debounceTimer);
169
+ if (this._previousActiveEl && document.contains(this._previousActiveEl)) {
170
+ this._previousActiveEl.focus?.();
171
+ }
172
+ this._previousActiveEl = null;
173
+ }
174
+
175
+ toggle() {
176
+ this._build();
177
+ if (this.overlay.hidden) this.show();
178
+ else this.hide();
179
+ }
180
+
181
+ isOpen() {
182
+ return !!this._built && !this.overlay.hidden;
183
+ }
184
+
185
+ _scheduleChange() {
186
+ clearTimeout(this._debounceTimer);
187
+ this._debounceTimer = setTimeout(() => this._refresh(), DEBOUNCE_MS);
188
+ }
189
+
190
+ _onKey(e) {
191
+ switch (e.key) {
192
+ case 'Escape':
193
+ e.preventDefault();
194
+ this.hide();
195
+ break;
196
+ case 'Backspace':
197
+ // Nothing left to delete → hand back to the fuzzy quick
198
+ // switcher (mirror of the switcher's `@` → Open dialog handoff).
199
+ // With text present, fall through to native character delete.
200
+ if (this.input.value === '') {
201
+ e.preventDefault();
202
+ this.hide();
203
+ window.QuickSwitcher?.show();
204
+ }
205
+ break;
206
+ case 'Enter':
207
+ e.preventDefault();
208
+ if (this._items.length) this._submit(this._optsFromEvent(e));
209
+ break;
210
+ case 'ArrowDown':
211
+ e.preventDefault();
212
+ this._move(1);
213
+ break;
214
+ case 'ArrowUp':
215
+ e.preventDefault();
216
+ this._move(-1);
217
+ break;
218
+ case 'Tab':
219
+ e.preventDefault();
220
+ // Tab: accept the ghost if present, otherwise cycle the list
221
+ // so the user can still navigate when nothing's ahead of them.
222
+ if (this._ghost) this._acceptGhost();
223
+ else this._move(e.shiftKey ? -1 : 1);
224
+ break;
225
+ case 'ArrowRight':
226
+ // Modifier held (shift/alt/ctrl/meta) → user is selecting or
227
+ // word-jumping; let the input handle it natively.
228
+ if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey) break;
229
+ // Only intercept at end-of-input — otherwise the user is moving
230
+ // the caret inside the path and we shouldn't steal it.
231
+ // Priority: accept ghost → drill into selected dir.
232
+ if (this.input.selectionStart === this.input.value.length) {
233
+ if (this._ghost) {
234
+ e.preventDefault();
235
+ this._acceptGhost();
236
+ } else {
237
+ const item = this._items[this._selectedIndex];
238
+ // `is_open_here` is an action, not a deeper dir —
239
+ // skip it. `..` is fine (drill-in == go up).
240
+ if (item?.is_dir && !item.is_open_here) {
241
+ e.preventDefault();
242
+ this._drillInto(item);
243
+ }
244
+ }
245
+ }
246
+ break;
247
+ case 'ArrowLeft': {
248
+ if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey) break;
249
+ // Intercept when the caret is at position 0 (anywhere in the
250
+ // input) OR at the very end of a path that already ends with
251
+ // `/` (i.e. we're "in" a directory rather than editing a
252
+ // basename). Otherwise the caret belongs to the user.
253
+ const val = this.input.value;
254
+ const start = this.input.selectionStart;
255
+ const end = this.input.selectionEnd;
256
+ const noSelection = start === end;
257
+ const atStart = noSelection && start === 0;
258
+ const atEnd = noSelection && start === val.length;
259
+ if (atStart || (atEnd && /[\\/]$/.test(val))) {
260
+ e.preventDefault();
261
+ this._goUp();
262
+ }
263
+ break;
264
+ }
265
+ case 'End':
266
+ // Shift+End / Cmd+Shift+End etc are text-selection — leave alone.
267
+ if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey) break;
268
+ if (this._ghost) {
269
+ e.preventDefault();
270
+ this._acceptGhost();
271
+ }
272
+ break;
273
+ }
274
+ }
275
+
276
+ _goUp() {
277
+ const { dir } = this._parse(this.input.value);
278
+ const parent = parentOf(dir);
279
+ if (!parent) return;
280
+ const display = this._displayFor(parent, true);
281
+ this.input.value = display;
282
+ this.input.selectionStart = this.input.selectionEnd = display.length;
283
+ this._ghost = '';
284
+ this._refresh();
285
+ }
286
+
287
+ _move(delta) {
288
+ if (!this._items.length) return;
289
+ const n = this._items.length;
290
+ this._selectedIndex = (this._selectedIndex + delta + n) % n;
291
+ // Ghost follows the selected item when it's a real entry, but stays
292
+ // pinned to the first real item when the user lands on a synthetic
293
+ // row (`..` or "Open this folder") — so Tab still autocompletes
294
+ // their typing instead of going silent.
295
+ this._setGhostFromItem(this._ghostItemFor());
296
+ this._renderGhost();
297
+ this._renderList();
298
+ this.list.querySelector('.qs-item.selected')
299
+ ?.scrollIntoView({ block: 'nearest', behavior: 'auto' });
300
+ }
301
+
302
+ _ghostItemFor() {
303
+ const synthetic = it => it.is_parent || it.is_open_here || it.is_create_here;
304
+ const selected = this._items[this._selectedIndex];
305
+ if (selected && !synthetic(selected)) return selected;
306
+ return this._items.find(it => !synthetic(it));
307
+ }
308
+
309
+ _setGhostFromItem(item) {
310
+ // Fish-style: no input → no ghost (placeholder owns that space).
311
+ // Never suggest the synthetic navigation rows (`..`, "Open this folder").
312
+ const val = this.input.value;
313
+ if (!item || !val || item.is_parent || item.is_open_here || item.is_create_here) { this._ghost = ''; return; }
314
+ const filter = this._currentFilter();
315
+ const target = item.is_dir ? item.name + '/' : item.name;
316
+ if (target.toLowerCase().startsWith(filter.toLowerCase())) {
317
+ // Preserve target's case for the remainder.
318
+ let rest = target.slice(filter.length);
319
+ // Bare path tokens (`~`, `.`, `..`) are missing the separator
320
+ // slash before the next segment — without this fix, Tab on `~`
321
+ // would yield `~dev/` instead of `~/dev/`.
322
+ if (val === '~' || val === '.' || val === '..') {
323
+ rest = '/' + rest;
324
+ }
325
+ this._ghost = rest;
326
+ } else {
327
+ this._ghost = '';
328
+ }
329
+ }
330
+
331
+ _currentFilter() {
332
+ const { filter } = this._parse(this.input.value);
333
+ return filter;
334
+ }
335
+
336
+ _optsFromEvent(e) {
337
+ const mod = e.metaKey || e.ctrlKey;
338
+ if (mod && e.shiftKey) return { newTab: true };
339
+ if (mod) return { background: true };
340
+ return {};
341
+ }
342
+
343
+ /**
344
+ * Parse the input into a mode + (dir, filter) pair.
345
+ *
346
+ * Modes:
347
+ * - 'empty': "" → show cwd contents
348
+ * - 'basename': "app" → prefix-match basenames across the project tree
349
+ * - 'path': "src/", "~/dev/", "/etc/p", "../tests" → strict dir mode
350
+ */
351
+ _parse(input) {
352
+ const cwd = this._getCwd();
353
+ if (!input) return { mode: 'empty', dir: cwd, filter: '' };
354
+
355
+ const startsWithTilde = input === '~' || /^~[\\/]/.test(input);
356
+ const isAbsolute = isAbsolutePath(input, cwd);
357
+ const isRelative =
358
+ input === '.' || input === '..' ||
359
+ /^\.\.?[\\/]/.test(input);
360
+ // Either separator: a Windows user types (and pastes) backslashes.
361
+ const hasSlash = /[\\/]/.test(input);
362
+
363
+ const pathMode = startsWithTilde || isAbsolute || isRelative || hasSlash;
364
+ if (!pathMode) return { mode: 'basename', dir: cwd, filter: input };
365
+
366
+ // Split into (head) + (filter being typed after the last separator).
367
+ const lastSlash = Math.max(input.lastIndexOf('/'), input.lastIndexOf('\\'));
368
+ let head, filter;
369
+ if (lastSlash === -1) {
370
+ // Input is `~`, `.`, or `..` — treat as if it had a trailing slash.
371
+ head = input + '/';
372
+ filter = '';
373
+ } else {
374
+ head = input.slice(0, lastSlash + 1);
375
+ filter = input.slice(lastSlash + 1);
376
+ }
377
+
378
+ let dir;
379
+ if (/^~[\\/]/.test(head)) {
380
+ dir = joinPath(CONFIG.HOME || '', head.slice(2));
381
+ } else if (isAbsolutePath(head, cwd)) {
382
+ dir = head;
383
+ } else {
384
+ // ./foo/ ../foo/ foo/bar/ (relative to cwd)
385
+ dir = resolvePath(cwd, head);
386
+ }
387
+ dir = stripTrailingSep(dir) || pathRoot(dir) || '/';
388
+ return { mode: 'path', dir, filter };
389
+ }
390
+
391
+ _getCwd() {
392
+ // Prefer the active session's cwd; on the welcome screen there's none
393
+ // yet, so fall back to the server's --workspace dir (mirrors the file
394
+ // explorer anchor; CONFIG.WORKSPACE itself falls back to the OS home
395
+ // when --workspace isn't set).
396
+ const sessionCwd = window.app?.activeSession?.cwd;
397
+ if (sessionCwd) return sessionCwd;
398
+ return window.app?.lastCwd || CONFIG.WORKSPACE;
399
+ }
400
+
401
+ async _refresh() {
402
+ const value = this.input.value;
403
+ const { mode, dir, filter } = this._parse(value);
404
+ this._renderCurrentPath(mode, dir);
405
+ this._missingDir = null;
406
+
407
+ const token = ++this._reqToken;
408
+ let items = [];
409
+ try {
410
+ if (mode === 'empty') {
411
+ items = await this._listDir(dir, '');
412
+ } else if (mode === 'basename') {
413
+ items = await this._projectBasenameMatches(dir, filter);
414
+ } else {
415
+ items = await this._listDir(dir, filter);
416
+ }
417
+ } catch (err) {
418
+ console.error('[OpenDialog]', err);
419
+ }
420
+ if (token !== this._reqToken) return;
421
+
422
+ // Typed a path that doesn't exist → offer to create it. Covers a
423
+ // fully-typed missing dir (`~/dev/newproj/`, listing 404s), a new
424
+ // name typed inside an existing dir (`~/dev/newproj`, prefix filter
425
+ // matches nothing), and a bare name with no slash at all
426
+ // (`newproj`, basename search over the project comes up empty —
427
+ // created relative to cwd). mkdir -p on the server also handles
428
+ // missing intermediate dirs.
429
+ const offerCreate = items.length === 0 && (
430
+ (mode === 'path' && (filter || this._missingDir === dir)) ||
431
+ (mode === 'basename' && filter)
432
+ );
433
+ if (offerCreate) {
434
+ const target = filter ? joinPath(dir, filter) : dir;
435
+ const pretty = this._formatPathForDisplay(target).replace(/[\\/]$/, '') || '/';
436
+ items = [{
437
+ type: 'action',
438
+ is_dir: true,
439
+ is_create_here: true,
440
+ name: '__create_here__',
441
+ label: S.open_dialog.create_here.label,
442
+ description: S.open_dialog.create_here.desc.replace('{dir}', pretty),
443
+ absPath: target,
444
+ }];
445
+ }
446
+
447
+ this._items = items;
448
+ // Default-select the "Open this folder" action when it's present
449
+ // (so plain Enter opens the listed dir as a session). With `..`
450
+ // also added it sits at index 1; without `..` it's at index 0.
451
+ // Falls back to 0 when neither synthetic row exists (filter mode).
452
+ const ohIdx = items.findIndex(it => it.is_open_here);
453
+ this._selectedIndex = ohIdx >= 0 ? ohIdx : 0;
454
+ this._setGhostFromItem(this._ghostItemFor());
455
+ this._renderGhost();
456
+ this._renderList();
457
+ }
458
+
459
+ async _listDir(dir, filter) {
460
+ if (!dir) return [];
461
+
462
+ const cached = this._pathCache.get(dir);
463
+ let entries;
464
+ if (cached && Date.now() - cached.t < PATH_CACHE_TTL) {
465
+ entries = cached.entries;
466
+ } else {
467
+ const r = await fetch(`${CONFIG.API_BASE}/api/files?path=${encodeURIComponent(dir)}`);
468
+ if (!r.ok) {
469
+ // 404 → the dir doesn't exist yet; _refresh offers to create it.
470
+ // Other errors (403/timeout) just yield an empty listing.
471
+ if (r.status === 404) this._missingDir = dir;
472
+ return [];
473
+ }
474
+ const data = await r.json();
475
+ entries = data.files || [];
476
+ this._pathCache.set(dir, { t: Date.now(), entries });
477
+ }
478
+
479
+ const fLower = filter.toLowerCase();
480
+ const filtered = filter
481
+ ? entries.filter(e => e.name.toLowerCase().startsWith(fLower))
482
+ // Hide dotfiles by default; user can type `.` to opt in.
483
+ : entries.filter(e => !e.name.startsWith('.'));
484
+
485
+ filtered.sort((a, b) => {
486
+ if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
487
+ return a.name.localeCompare(b.name);
488
+ });
489
+
490
+ const mapped = filtered.slice(0, MAX_LIST).map(e => ({
491
+ type: e.is_dir ? 'dir' : 'file',
492
+ is_dir: e.is_dir,
493
+ name: e.name,
494
+ label: e.is_dir ? e.name + '/' : e.name,
495
+ description: '',
496
+ absPath: e.path,
497
+ }));
498
+
499
+ // Synthetic rows: `..` (parent) at the very top so it matches the
500
+ // typical file-manager mental model, then "Open this folder" which
501
+ // becomes the default-selected action (Enter opens the listed dir
502
+ // as a new session). Only added when no filter is active — prefix-
503
+ // match would hide them anyway and they'd be confusing alongside
504
+ // filtered file results.
505
+ if (!filter) {
506
+ // Unshift order is reverse of final order — push the action
507
+ // first so it ends up at index 1 (or 0 when there's no parent).
508
+ mapped.unshift({
509
+ type: 'action',
510
+ is_dir: true,
511
+ is_open_here: true,
512
+ name: '__open_here__',
513
+ label: S.open_dialog.open_here.label,
514
+ description: S.open_dialog.open_here.desc,
515
+ absPath: dir,
516
+ });
517
+ const parent = parentOf(dir);
518
+ if (parent) {
519
+ mapped.unshift({
520
+ type: 'dir',
521
+ is_dir: true,
522
+ is_parent: true,
523
+ name: '..',
524
+ label: '../',
525
+ description: '',
526
+ absPath: parent,
527
+ });
528
+ }
529
+ }
530
+ return mapped;
531
+ }
532
+
533
+ async _projectBasenameMatches(cwd, filter) {
534
+ if (!cwd || !filter) return [];
535
+
536
+ const cached = this._projectCache.get(cwd);
537
+ let payload;
538
+ if (cached && Date.now() - cached.t < PROJECT_CACHE_TTL) {
539
+ payload = cached.payload;
540
+ } else {
541
+ // include_ignored=true: the user is picking a file to open, so
542
+ // .gitignore'd files (CLAUDE.md, .env, local notes) must be reachable
543
+ // — otherwise typing the exact basename returns "No matches" even
544
+ // though the file is right there on disk.
545
+ const r = await fetch(`${CONFIG.API_BASE}/api/files/list?cwd=${encodeURIComponent(cwd)}&include_ignored=true`);
546
+ if (!r.ok) return [];
547
+ payload = await r.json();
548
+ this._projectCache.set(cwd, { t: Date.now(), payload });
549
+ }
550
+
551
+ const recent = await this._loadRecent(cwd);
552
+ const fLower = filter.toLowerCase();
553
+ const files = payload.files || [];
554
+ const dirs = payload.directories || [];
555
+
556
+ const matches = [];
557
+ for (const d of dirs) {
558
+ const name = d.replace(/\/$/, '');
559
+ if (!name.toLowerCase().startsWith(fLower)) continue;
560
+ const abs = resolvePath(cwd, name);
561
+ matches.push({
562
+ type: 'dir',
563
+ is_dir: true,
564
+ name,
565
+ label: name + '/',
566
+ description: cwd,
567
+ absPath: abs,
568
+ recentRank: recent.get(abs) ?? Infinity,
569
+ });
570
+ }
571
+ for (const path of files) {
572
+ const fileName = basename(path);
573
+ if (!fileName.toLowerCase().startsWith(fLower)) continue;
574
+ const abs = resolvePath(cwd, path);
575
+ matches.push({
576
+ type: 'file',
577
+ is_dir: false,
578
+ name: fileName,
579
+ label: fileName,
580
+ description: dirname(path),
581
+ absPath: abs,
582
+ recentRank: recent.get(abs) ?? Infinity,
583
+ });
584
+ }
585
+
586
+ // Recent first, then dirs over files, then shallower paths, then alpha.
587
+ matches.sort((a, b) => {
588
+ if (a.recentRank !== b.recentRank) return a.recentRank - b.recentRank;
589
+ if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
590
+ const ad = splitPath(a.description, a.absPath).length;
591
+ const bd = splitPath(b.description, b.absPath).length;
592
+ if (ad !== bd) return ad - bd;
593
+ return a.name.localeCompare(b.name);
594
+ });
595
+
596
+ return matches.slice(0, MAX_BASENAME_LIST);
597
+ }
598
+
599
+ async _loadRecent(cwd) {
600
+ const cached = this._recentCache.get(cwd);
601
+ if (cached && Date.now() - cached.t < RECENT_TTL) return cached.map;
602
+ const map = new Map();
603
+ try {
604
+ const r = await fetch(
605
+ `${CONFIG.API_BASE}/api/shadow-db/recent-files?cwd=${encodeURIComponent(cwd)}&limit=100`
606
+ );
607
+ if (r.ok) {
608
+ const data = await r.json();
609
+ (data.files || []).forEach((f, i) => {
610
+ const abs = resolvePath(cwd, f.path);
611
+ map.set(abs, i);
612
+ });
613
+ }
614
+ } catch {
615
+ // best-effort; ghost still works without recency data
616
+ }
617
+ this._recentCache.set(cwd, { t: Date.now(), map });
618
+ return map;
619
+ }
620
+
621
+ _renderGhost() {
622
+ this.ghostTyped.textContent = this.input.value;
623
+ this.ghostRest.textContent = this._ghost || '';
624
+ this._updateFooterForSelection();
625
+ }
626
+
627
+ _renderCurrentPath(mode, dir) {
628
+ if (!this.pathEl) return;
629
+ const pretty = this._formatPathForDisplay(dir);
630
+ if (mode === 'basename') {
631
+ this.pathEl.innerHTML =
632
+ `<span class="od-path-label">${escapeHtml(S.open_dialog.section.project)}</span>` +
633
+ `<span class="od-path-text">${escapeHtml(pretty)}</span>`;
634
+ } else {
635
+ this.pathEl.innerHTML = `<span class="od-path-text">${escapeHtml(pretty)}</span>`;
636
+ }
637
+ }
638
+
639
+ _formatPathForDisplay(dir) {
640
+ if (!dir) return '';
641
+ const home = CONFIG.HOME || '';
642
+ let out = dir;
643
+ if (home && isUnder(dir, home)) {
644
+ out = '~' + dir.slice(home.length);
645
+ }
646
+ return /[\\/]$/.test(out) ? out : out + pathSep(dir);
647
+ }
648
+
649
+ _renderList() {
650
+ if (!this._items.length) {
651
+ const txt = this.input.value.trim() ? S.open_dialog.empty.no_results : ' ';
652
+ this.list.innerHTML = `<div class="qs-empty">${escapeHtml(txt)}</div>`;
653
+ this._updateFooterForSelection();
654
+ return;
655
+ }
656
+ const parts = [];
657
+ for (let i = 0; i < this._items.length; i++) {
658
+ parts.push(this._renderItem(this._items[i], i, i === this._selectedIndex));
659
+ }
660
+ this.list.innerHTML = parts.join('');
661
+ this.input.setAttribute('aria-activedescendant', `od-item-${this._selectedIndex}`);
662
+ this._updateFooterForSelection();
663
+ }
664
+
665
+ _renderItem(item, index, isSelected) {
666
+ const filter = this._currentFilter();
667
+ const label = item.label;
668
+ let labelHtml = escapeHtml(label);
669
+ if (filter && label.toLowerCase().startsWith(filter.toLowerCase())) {
670
+ labelHtml =
671
+ `<mark>${escapeHtml(label.slice(0, filter.length))}</mark>` +
672
+ escapeHtml(label.slice(filter.length));
673
+ }
674
+ const icon = (item.is_open_here || item.is_create_here)
675
+ ? ICONS.plus
676
+ : (item.is_dir ? ICONS.folder : ICONS.file);
677
+ const desc = item.description
678
+ ? `<span class="qs-item-desc">${escapeHtml(item.description)}</span>`
679
+ : '';
680
+ const extraCls = (item.is_open_here || item.is_create_here) ? ' qs-item-open-here'
681
+ : (item.is_parent ? ' qs-item-parent' : '');
682
+ const cls = `qs-item qs-item-${item.type}${extraCls}${isSelected ? ' selected' : ''}`;
683
+ return `
684
+ <div class="${cls}" role="option" data-index="${index}" id="od-item-${index}" aria-selected="${isSelected}">
685
+ <span class="qs-item-icon">${icon}</span>
686
+ <span class="qs-item-text">
687
+ <span class="qs-item-label">${labelHtml}</span>
688
+ ${desc}
689
+ </span>
690
+ </div>
691
+ `;
692
+ }
693
+
694
+ _updateFooterForSelection() {
695
+ if (!this.footer) return;
696
+ const item = this._items[this._selectedIndex];
697
+ const isDir = !!item?.is_dir;
698
+ // "Open this folder" commits a session on Enter; "Create this folder"
699
+ // mkdirs + drills in; every other dir (regular folders + `..`) drills
700
+ // in. Footer hint follows suit.
701
+ const isOpenAction = !!item?.is_open_here;
702
+ const isCreateAction = !!item?.is_create_here;
703
+ this.footer.classList.toggle('od-state-file', !!(item && !isDir));
704
+ this.footer.classList.toggle('od-state-dir-nav', isDir && !isOpenAction && !isCreateAction);
705
+ this.footer.classList.toggle('od-state-dir-create', isCreateAction);
706
+ this.footer.classList.toggle('od-state-dir-open', isDir && isOpenAction);
707
+ this.footer.classList.toggle('od-state-ghost', !!this._ghost);
708
+ }
709
+
710
+ _acceptGhost() {
711
+ if (!this._ghost) return;
712
+ const newValue = this.input.value + this._ghost;
713
+ this.input.value = newValue;
714
+ this.input.selectionStart = this.input.selectionEnd = newValue.length;
715
+ this._ghost = '';
716
+ this._renderGhost();
717
+ // After accepting, if the new value ends with '/', _refresh will fetch
718
+ // the new directory's contents and pick a fresh ghost.
719
+ this._refresh();
720
+ }
721
+
722
+ async _submit(opts = {}) {
723
+ const item = this._items[this._selectedIndex];
724
+ if (!item) return;
725
+ // The synthetic `..` is a navigation row, not a destination. Walk up
726
+ // and keep the dialog open instead of opening a session.
727
+ if (item.is_parent) { this._goUp(); return; }
728
+ // "Create this folder" → mkdir then drill into it, keeping the dialog
729
+ // open. The freshly-listed dir offers "Open this folder" as the
730
+ // default-selected row, so a session is still just one Enter away.
731
+ if (item.is_create_here) {
732
+ await this._createAndEnter(item.absPath);
733
+ return;
734
+ }
735
+ // A regular folder → drill into it (browse), keeping the dialog open.
736
+ // Opening as a session is reserved for the explicit "Open this folder"
737
+ // row, which is auto-selected after every drill-in (one Enter away).
738
+ if (item.is_dir && !item.is_open_here) {
739
+ this._drillInto(item);
740
+ return;
741
+ }
742
+ this.hide();
743
+ try {
744
+ if (item.is_dir) this._openSessionAt(item.absPath);
745
+ else this._openFile(item.absPath, opts);
746
+ } catch (err) {
747
+ console.error('[OpenDialog] submit:', err);
748
+ }
749
+ }
750
+
751
+ async _createAndEnter(dir) {
752
+ try {
753
+ const r = await fetch(
754
+ `${CONFIG.API_BASE}/api/mkdir`,
755
+ {
756
+ method: 'POST',
757
+ headers: { 'Content-Type': 'application/json' },
758
+ body: JSON.stringify({ path: dir }),
759
+ }
760
+ );
761
+ const data = await r.json().catch(() => ({}));
762
+ if (!r.ok) {
763
+ // `detail` is a *list* of dicts on a 422 (Pydantic) — stringifying
764
+ // it raw is what turned this toast into "[object Object]".
765
+ showToast(S.open_dialog.create_failed.replace(
766
+ '{error}', extractApiError(data, `HTTP ${r.status}`)));
767
+ return;
768
+ }
769
+ // Bust the cached listings (the dir itself + its parent, whose
770
+ // cached entry list is now missing the new child).
771
+ this._pathCache.delete(dir);
772
+ const parent = parentOf(dir);
773
+ if (parent) this._pathCache.delete(parent);
774
+ this._drillInto({ is_dir: true, absPath: data.path || dir });
775
+ this.input.focus();
776
+ } catch (err) {
777
+ console.error('[OpenDialog] create:', err);
778
+ showToast(S.open_dialog.create_failed.replace('{error}', err.message));
779
+ }
780
+ }
781
+
782
+ _openFile(absPath, opts = {}) {
783
+ const tabCtrl = window.app?.tabCtrl;
784
+ if (opts.background && tabCtrl?.openFilePreviewTab) {
785
+ tabCtrl.openFilePreviewTab(absPath, null, { background: true });
786
+ return;
787
+ }
788
+ if (opts.newTab && tabCtrl?.openFilePreviewTab) {
789
+ tabCtrl.openFilePreviewTab(absPath, null, { newTab: true });
790
+ return;
791
+ }
792
+ window.app?.previewFile?.(absPath);
793
+ }
794
+
795
+ _openSessionAt(cwd) {
796
+ const app = window.app;
797
+ if (!app?.sessionManager) return;
798
+ const name = basename(cwd) || 'New Session';
799
+ const session = app.sessionManager.create({ name });
800
+ if (!session) return;
801
+ session.cwd = cwd;
802
+ if (app.tabCtrl?.switchToSession) app.tabCtrl.switchToSession(session);
803
+ else app.switchSession?.(session);
804
+ app.renderTabs?.();
805
+ session.connect();
806
+ app.sessionManager.saveSessions?.();
807
+ app.addToHistory?.(cwd);
808
+ app.fileExplorer?.setHomePath?.(cwd);
809
+ app.fetchProjectCommands?.(cwd);
810
+ app.els?.connectionBar?.classList.remove('visible');
811
+ // If the user typed a message on the welcome screen before picking
812
+ // a project, send it now on the new session.
813
+ if (app._pendingWelcomeMessage) {
814
+ const msg = app._pendingWelcomeMessage;
815
+ app._pendingWelcomeMessage = null;
816
+ if (app.els?.messageInput) app.els.messageInput.value = '';
817
+ app.syncInputHighlightBackdrop?.();
818
+ app._sendMessageDirect?.(msg);
819
+ }
820
+ }
821
+
822
+ _showContextMenu(x, y) {
823
+ const item = this._items[this._selectedIndex];
824
+ if (!item) return;
825
+ const menu = window.app?.contextMenu || (this._ctxMenu ||= new ContextMenu());
826
+ const tabCtrl = window.app?.tabCtrl;
827
+ const close = (fn) => { this.hide(); fn?.(); };
828
+ const items = item.is_dir
829
+ ? [
830
+ { label: S.open_dialog.menu.open_session, action: () => close(() => this._openSessionAt(item.absPath)) },
831
+ { label: S.open_dialog.menu.drill_in, action: () => { this._drillInto(item); this.input.focus(); } },
832
+ { type: 'separator' },
833
+ { label: S.context_menus.file.copy_path, action: async () => { if (await copyToClipboard(item.absPath)) showToast(S.toast.copied); } },
834
+ ]
835
+ : [
836
+ { label: S.context_menus.file.preview, action: () => close(() => window.app?.previewFile?.(item.absPath)) },
837
+ { label: S.context_menus.file.open_in_new_tab, action: () => close(() => tabCtrl?.openFilePreviewTab?.(item.absPath, null, { newTab: true })) },
838
+ { label: S.context_menus.file.open_in_background, action: () => close(() => tabCtrl?.openFilePreviewTab?.(item.absPath, null, { background: true })) },
839
+ { type: 'separator' },
840
+ { label: S.context_menus.file.copy_full_path, action: async () => { if (await copyToClipboard(item.absPath)) showToast(S.toast.copied); } },
841
+ ];
842
+ menu.show(x, y, items);
843
+ }
844
+
845
+ _drillInto(item) {
846
+ if (!item?.is_dir) return;
847
+ const display = this._displayFor(item.absPath, true);
848
+ this.input.value = display;
849
+ this.input.selectionStart = this.input.selectionEnd = display.length;
850
+ this._ghost = '';
851
+ this._refresh();
852
+ }
853
+
854
+ _displayFor(absPath, isDir) {
855
+ const cwd = this._getCwd();
856
+ const home = CONFIG.HOME || '';
857
+ const original = this.input.value;
858
+ let out;
859
+ if (home && absPath === home) {
860
+ out = '~';
861
+ } else if (/^~[\\/]/.test(original) && home && isUnder(absPath, home) && absPath !== home) {
862
+ out = '~' + pathSep(absPath) + relDisplay(absPath, home);
863
+ } else if (isAbsolutePath(original, cwd) || !cwd) {
864
+ out = absPath;
865
+ } else if (absPath === cwd) {
866
+ out = './';
867
+ } else if (isUnder(absPath, cwd) && absPath !== cwd) {
868
+ out = relDisplay(absPath, cwd);
869
+ } else if (home && isUnder(absPath, home) && absPath !== home) {
870
+ out = '~' + pathSep(absPath) + relDisplay(absPath, home);
871
+ } else {
872
+ out = absPath;
873
+ }
874
+ if (isDir && !/[\\/]$/.test(out)) out += pathSep(absPath);
875
+ return out;
876
+ }
877
+ }
878
+
879
+ export const OpenDialog = new OpenDialogClass();